├── example_app ├── .ruby-version ├── .cfignore ├── config.ru ├── Rakefile ├── test │ ├── test_helper.rb │ ├── integration │ │ ├── integration_test_helper.rb │ │ └── github_integration_test.rb │ ├── github_repo_helper_test.rb │ └── service_consumer_app_test.rb ├── Gemfile ├── views │ └── index.erb ├── Gemfile.lock ├── service_consumer_app.rb └── github_repo_helper.rb ├── service_broker ├── .ruby-version ├── Dockerfile ├── Rakefile ├── test │ ├── fixtures │ │ ├── github_resource_not_found_response.json │ │ ├── create_github_deploy_key_success_response.json │ │ ├── list_github_deploy_keys.json │ │ ├── github_general_error_response.json │ │ ├── create_github_repo_failure_already_exists_response.json │ │ ├── create_github_deploy_key_failure.json │ │ └── create_github_repo_success_response.json │ ├── test_helper.rb │ ├── config │ │ └── settings.yml │ ├── service_broker_app_test.rb │ └── github_service_helper_test.rb ├── .cfignore ├── config.ru ├── Gemfile ├── Gemfile.lock ├── config │ └── settings.yml ├── service_broker_app.rb └── github_service_helper.rb ├── .gitignore ├── ci.sh ├── .travis.yml ├── README.md └── LICENSE /example_app/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0-p353 2 | -------------------------------------------------------------------------------- /service_broker/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0-p353 2 | -------------------------------------------------------------------------------- /service_broker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.0.0 2 | RUN mkdir /sb 3 | COPY . /sb 4 | WORKDIR /sb 5 | RUN bundle install 6 | CMD rackup 7 | -------------------------------------------------------------------------------- /service_broker/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new do |t| 4 | t.pattern = "test/*_test.rb" 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | 3 | .idea 4 | /service_broker/manifest.yml 5 | /example_app/manifest.yml 6 | -------------------------------------------------------------------------------- /service_broker/test/fixtures/github_resource_not_found_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Not Found", 3 | "documentation_url": "http://developer.github.com/v3" 4 | } 5 | -------------------------------------------------------------------------------- /example_app/.cfignore: -------------------------------------------------------------------------------- 1 | # files of the following types are ignored when pushing to Cloud Foundry 2 | # see http://docs.cloudfoundry.com/docs/using/deploying-apps/ 3 | 4 | /.idea 5 | /test -------------------------------------------------------------------------------- /service_broker/.cfignore: -------------------------------------------------------------------------------- 1 | # files of the following types are ignored when pushing to Cloud Foundry 2 | # see http://docs.cloudfoundry.com/docs/using/deploying-apps/ 3 | 4 | /.idea 5 | /test -------------------------------------------------------------------------------- /service_broker/test/fixtures/create_github_deploy_key_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "key": "ssh-rsa AAA...", 4 | "url": "https://api.github.com/user/keys/1", 5 | "title": "octocat@octomac" 6 | } -------------------------------------------------------------------------------- /example_app/config.ru: -------------------------------------------------------------------------------- 1 | # This rack config file is used to start the service consumer application 2 | # when it is deployed as an application on Cloud Foundry 3 | 4 | require './service_consumer_app' 5 | run ServiceConsumerApp.new -------------------------------------------------------------------------------- /service_broker/config.ru: -------------------------------------------------------------------------------- 1 | # This rack config file is used to start the service broker application 2 | # when it is deployed as an application on Cloud Foundry 3 | 4 | require './service_broker_app' 5 | run ServiceBrokerApp.new -------------------------------------------------------------------------------- /example_app/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new do |t| 4 | t.test_files = FileList.new("test/*_test.rb") 5 | end 6 | 7 | Rake::TestTask.new(:integration_test) do |t| 8 | t.pattern = "test/integration/*_test.rb" 9 | end 10 | -------------------------------------------------------------------------------- /example_app/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RACK_ENV'] = 'test' 2 | require 'minitest/autorun' 3 | require 'minitest/spec' 4 | require 'rack/test' 5 | require 'mocha/setup' 6 | require 'pry' 7 | 8 | require File.expand_path '../../service_consumer_app.rb', __FILE__ 9 | require File.expand_path '../../github_repo_helper.rb', __FILE__ -------------------------------------------------------------------------------- /service_broker/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby "2.0.0" 3 | 4 | gem 'sinatra' 5 | gem 'json' 6 | gem 'octokit' 7 | gem 'sshkey' 8 | gem 'rake' 9 | 10 | group :test do 11 | gem 'codeclimate-test-reporter', require: nil 12 | gem 'rack-test' 13 | gem 'mocha', require: false 14 | gem 'webmock' 15 | gem 'pry' 16 | end -------------------------------------------------------------------------------- /example_app/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby "2.0.0" 3 | 4 | gem 'sinatra' 5 | gem 'rack-flash3' 6 | gem 'cf-app-utils' 7 | gem 'rake' 8 | 9 | group :test do 10 | gem 'rack-test' 11 | gem 'mocha', require: false 12 | gem 'rerun' 13 | gem 'capybara' 14 | gem 'capybara_minitest_spec' 15 | gem 'octokit' 16 | gem 'pry' 17 | end -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | echo && \ 2 | echo && \ 3 | echo ">>> RUNNING TESTS FOR SERVICE BROKER" && \ 4 | cd service_broker && \ 5 | bundle install --deployment && \ 6 | bundle exec rake test && \ 7 | echo && \ 8 | echo && \ 9 | echo ">>> RUNNING TESTS FOR SERVICE CONSUMER" && \ 10 | cd ../example_app && \ 11 | bundle install --deployment && \ 12 | bundle exec rake test && \ 13 | cd .. 14 | -------------------------------------------------------------------------------- /example_app/test/integration/integration_test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'capybara' 2 | require 'capybara/dsl' 3 | require 'capybara_minitest_spec' 4 | require 'octokit' 5 | 6 | 7 | Octokit.auto_paginate = true 8 | 9 | Capybara.app = ServiceConsumerApp 10 | 11 | class MiniTest::Spec 12 | include Capybara::DSL 13 | end 14 | 15 | after :all do 16 | Capybara.reset_sessions! 17 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - develop 4 | - master 5 | language: ruby 6 | rvm: 7 | - 2.0.0 8 | before_script: 9 | - chmod +x ci.sh 10 | script: ./ci.sh 11 | env: 12 | global: 13 | secure: UwZfR1zlq+42af+6PACVJ+e9qcaHo2ibXiEbFQHQXkCj9cG0DLmdJ6LrhZ9WO0Za/HlB5Lv9988xWV/VGkH10K3n1e59xzEggDYCLpaR3L+YsPHbvX6q9GHKowePo+eztmwI5Vx65zHMMM+9G2mwzzyQBt3yOIG/k90+dUbGtPs= 14 | -------------------------------------------------------------------------------- /service_broker/test/fixtures/list_github_deploy_keys.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "key": "ssh-rsa AAA...", 5 | "url": "https://api.github.com/user/keys/1", 6 | "title": "first-key" 7 | }, 8 | { 9 | "id": 2, 10 | "key": "ssh-rsa BBB...", 11 | "url": "https://api.github.com/user/keys/2", 12 | "title": "second-key" 13 | } 14 | ] -------------------------------------------------------------------------------- /service_broker/test/fixtures/github_general_error_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message":"Something Failed", 3 | "documentation_url":"http://developer.github.com/v3/repos/#some-action", 4 | "errors":[ 5 | { 6 | "resource":"some resource", 7 | "code":"custom", 8 | "field":"some field", 9 | "message":"some error message" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /service_broker/test/fixtures/create_github_repo_failure_already_exists_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message":"Validation Failed", 3 | "documentation_url":"http://developer.github.com/v3/repos/#create", 4 | "errors":[ 5 | { 6 | "resource":"Repository", 7 | "code":"custom", 8 | "field":"name", 9 | "message":"name already exists on this account" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /service_broker/test/fixtures/create_github_deploy_key_failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "message":"Validation Failed", 3 | "documentation_url":"http://developer.github.com/v3/repos/keys/#create", 4 | "errors":[ 5 | { 6 | "resource":"PublicKey", 7 | "code":"custom", 8 | "field":"key", 9 | "message":"key is invalid. Ensure you've copied the file correctly" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /service_broker/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'codeclimate-test-reporter' 2 | CodeClimate::TestReporter.start 3 | 4 | ENV['RACK_ENV'] = 'test' 5 | require 'minitest/autorun' 6 | require 'minitest/spec' 7 | require 'rack/test' 8 | require 'mocha/setup' 9 | require 'webmock/minitest' 10 | require 'pry' 11 | 12 | WebMock.disable_net_connect!(allow: 'codeclimate.com') 13 | 14 | SETTINGS_FILENAME = "test/config/settings.yml" 15 | 16 | require File.expand_path '../../service_broker_app.rb', __FILE__ 17 | require File.expand_path '../../github_service_helper.rb', __FILE__ -------------------------------------------------------------------------------- /example_app/views/index.erb: -------------------------------------------------------------------------------- 1 | 10 | 11 | <% if flash[:notice] %> 12 |
<%= Rack::Utils.escape_html(flash[:notice]) %>
13 |
14 |
15 | <% end %> 16 | 17 | <% unless repo_uris.nil? %> 18 | 19 | 20 | 21 | 22 | 23 | <% repo_uris.each do |uri| %> 24 | 25 | 26 | 32 | 33 | <% end %> 34 |
Repo URL
<%= uri %> 27 |
28 | 29 | 30 |
31 |
35 |
36 | <% end %> 37 | 38 | <%= messages %> -------------------------------------------------------------------------------- /service_broker/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.5) 5 | codeclimate-test-reporter (0.3.0) 6 | simplecov (>= 0.7.1, < 1.0.0) 7 | coderay (1.1.0) 8 | crack (0.4.1) 9 | safe_yaml (~> 0.9.0) 10 | docile (1.1.3) 11 | faraday (0.8.8) 12 | multipart-post (~> 1.2.0) 13 | json (1.8.1) 14 | metaclass (0.0.1) 15 | method_source (0.8.2) 16 | mocha (0.14.0) 17 | metaclass (~> 0.0.1) 18 | multi_json (1.9.3) 19 | multipart-post (1.2.0) 20 | octokit (2.6.3) 21 | sawyer (~> 0.5.1) 22 | pry (0.9.12.4) 23 | coderay (~> 1.0) 24 | method_source (~> 0.8) 25 | slop (~> 3.4) 26 | rack (1.5.2) 27 | rack-protection (1.5.1) 28 | rack 29 | rack-test (0.6.2) 30 | rack (>= 1.0) 31 | rake (10.1.1) 32 | safe_yaml (0.9.7) 33 | sawyer (0.5.1) 34 | addressable (~> 2.3.5) 35 | faraday (~> 0.8, < 0.10) 36 | simplecov (0.8.2) 37 | docile (~> 1.1.0) 38 | multi_json 39 | simplecov-html (~> 0.8.0) 40 | simplecov-html (0.8.0) 41 | sinatra (1.4.4) 42 | rack (~> 1.4) 43 | rack-protection (~> 1.4) 44 | tilt (~> 1.3, >= 1.3.4) 45 | slop (3.4.7) 46 | sshkey (1.6.1) 47 | tilt (1.4.1) 48 | webmock (1.16.0) 49 | addressable (>= 2.2.7) 50 | crack (>= 0.3.2) 51 | 52 | PLATFORMS 53 | ruby 54 | 55 | DEPENDENCIES 56 | codeclimate-test-reporter 57 | json 58 | mocha 59 | octokit 60 | pry 61 | rack-test 62 | rake 63 | sinatra 64 | sshkey 65 | webmock 66 | -------------------------------------------------------------------------------- /example_app/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.5) 5 | capybara (2.2.1) 6 | mime-types (>= 1.16) 7 | nokogiri (>= 1.3.3) 8 | rack (>= 1.0.0) 9 | rack-test (>= 0.5.4) 10 | xpath (~> 2.0) 11 | capybara_minitest_spec (1.0.1) 12 | capybara (>= 2) 13 | minitest (>= 2) 14 | cf-app-utils (0.2) 15 | coderay (1.1.0) 16 | faraday (0.8.8) 17 | multipart-post (~> 1.2.0) 18 | ffi (1.9.3) 19 | listen (1.0.3) 20 | rb-fsevent (>= 0.9.3) 21 | rb-inotify (>= 0.9) 22 | rb-kqueue (>= 0.2) 23 | metaclass (0.0.1) 24 | method_source (0.8.2) 25 | mime-types (2.0) 26 | mini_portile (0.5.2) 27 | minitest (5.2.1) 28 | mocha (0.14.0) 29 | metaclass (~> 0.0.1) 30 | multipart-post (1.2.0) 31 | nokogiri (1.6.1) 32 | mini_portile (~> 0.5.0) 33 | octokit (2.7.0) 34 | sawyer (~> 0.5.2) 35 | pry (0.9.12.4) 36 | coderay (~> 1.0) 37 | method_source (~> 0.8) 38 | slop (~> 3.4) 39 | rack (1.5.2) 40 | rack-flash3 (1.0.5) 41 | rack 42 | rack-protection (1.5.1) 43 | rack 44 | rack-test (0.6.2) 45 | rack (>= 1.0) 46 | rake (10.1.1) 47 | rb-fsevent (0.9.4) 48 | rb-inotify (0.9.3) 49 | ffi (>= 0.5.0) 50 | rb-kqueue (0.2.0) 51 | ffi (>= 0.5.0) 52 | rerun (0.8.2) 53 | listen (~> 1.0.3) 54 | sawyer (0.5.2) 55 | addressable (~> 2.3.5) 56 | faraday (~> 0.8, < 0.10) 57 | sinatra (1.4.4) 58 | rack (~> 1.4) 59 | rack-protection (~> 1.4) 60 | tilt (~> 1.3, >= 1.3.4) 61 | slop (3.4.7) 62 | tilt (1.4.1) 63 | xpath (2.0.0) 64 | nokogiri (~> 1.3) 65 | 66 | PLATFORMS 67 | ruby 68 | 69 | DEPENDENCIES 70 | capybara 71 | capybara_minitest_spec 72 | cf-app-utils 73 | mocha 74 | octokit 75 | pry 76 | rack-flash3 77 | rack-test 78 | rake 79 | rerun 80 | sinatra 81 | -------------------------------------------------------------------------------- /service_broker/config/settings.yml: -------------------------------------------------------------------------------- 1 | catalog: 2 | services: # catalog must advertise at least one service 3 | - id: 5085f1cb-e093-4630-8795-843b76018eb8 4 | name: github-repo 5 | description: Provides read and write access to a GitHub repository. 6 | bindable: true 7 | tags: 8 | - github 9 | metadata: 10 | displayName: GitHub Repo 11 | imageUrl: https://raw2.github.com/github/media/master/octocats/blacktocat-32.png 12 | longDescription: The service creates repos under the configured GitHub organization. Binding an app creates a deploy key for the repo that the example app uses to make commits to the repo. 13 | providerDisplayName: GitHub 14 | documentationUrl: https://github.com/cloudfoundry-samples/github-service-broker-ruby 15 | supportUrl: https://github.com/cloudfoundry-samples/github-service-broker-ruby 16 | plans: # a service has one or more plans 17 | - id: c05855e7-a55d-4ba1-b462-7579df7514f4 18 | name: public 19 | description: All repositories are public 20 | metadata: 21 | bullets: 22 | - Repos are public 23 | costs: 24 | - amount: 25 | usd: 0.0 26 | unit: MONTHLY 27 | displayName: Public 28 | 29 | # credentials for Cloud Controller to authenticate with the broker 30 | basic_auth: 31 | username: admin 32 | password: password 33 | 34 | # credentials for broker to authenticate with the GitHub account 35 | github: 36 | # An access token is used in place of username/password to access your GitHub account. 37 | # To generate an access token run the following command then copy the value of "token" from the response. 38 | # curl -u -d '{"scopes": ["repo", "delete_repo"], "note": "CF Service Broker"}' https://api.github.com/authorizations 39 | username: # 40 | access_token: # 41 | -------------------------------------------------------------------------------- /service_broker/test/config/settings.yml: -------------------------------------------------------------------------------- 1 | catalog: 2 | services: # catalog must advertise at least one service 3 | - id: 5085f1cb-e093-4630-8795-843b76018eb8 4 | name: github-repo 5 | description: Provides read and write access to a GitHub repository. 6 | bindable: true 7 | tags: 8 | - github 9 | metadata: 10 | displayName: GitHub Repo 11 | imageUrl: https://raw2.github.com/github/media/master/octocats/blacktocat-32.png 12 | longDescription: The service creates repos under the configured GitHub organization. Binding an app creates a deploy key for the repo that the example app uses to make commits to the repo. 13 | providerDisplayName: GitHub 14 | documentationUrl: https://github.com/cloudfoundry-samples/github-service-broker-ruby 15 | supportUrl: https://github.com/cloudfoundry-samples/github-service-broker-ruby 16 | plans: # a service has one or more plans 17 | - id: c05855e7-a55d-4ba1-b462-7579df7514f4 18 | name: public 19 | description: All repositories are public 20 | metadata: 21 | bullets: 22 | - Repos are public 23 | costs: 24 | - amount: 25 | usd: 0.0 26 | unit: MONTHLY 27 | displayName: Public 28 | 29 | # credentials for Cloud Controller to authenticate with the broker 30 | basic_auth: 31 | username: admin 32 | password: password 33 | 34 | # credentials for broker to authenticate with the GitHub account 35 | github: 36 | username: not-used-in-tests 37 | access_token: not-used-in-tests 38 | # The access token is used in place of username/password to access your GitHub account. 39 | # To generate the access token, run the following command, 40 | # then copy the value from the "token" field in the response: 41 | # curl -u -d '{"scopes": ["repo", "delete_repo"], "note": "my fancy token"}' https://api.github.com/authorizations -------------------------------------------------------------------------------- /service_broker/test/fixtures/create_github_repo_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1296269, 3 | "owner": { 4 | "login": "octocat", 5 | "id": 1, 6 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 7 | "gravatar_id": "somehexcode", 8 | "url": "https://api.github.com/users/octocat", 9 | "html_url": "https://github.com/octocat", 10 | "followers_url": "https://api.github.com/users/octocat/followers", 11 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 12 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 13 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 14 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 15 | "organizations_url": "https://api.github.com/users/octocat/orgs", 16 | "repos_url": "https://api.github.com/users/octocat/repos", 17 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 18 | "received_events_url": "https://api.github.com/users/octocat/received_events", 19 | "type": "User", 20 | "site_admin": false 21 | }, 22 | "name": "Hello-World", 23 | "full_name": "octocat/Hello-World", 24 | "description": "This your first repo!", 25 | "private": false, 26 | "fork": true, 27 | "url": "https://api.github.com/repos/octocat/Hello-World", 28 | "html_url": "https://github.com/octocat/Hello-World", 29 | "clone_url": "https://github.com/octocat/Hello-World.git", 30 | "git_url": "git://github.com/octocat/Hello-World.git", 31 | "ssh_url": "git@github.com:octocat/Hello-World.git", 32 | "svn_url": "https://svn.github.com/octocat/Hello-World", 33 | "mirror_url": "git://git.example.com/octocat/Hello-World", 34 | "homepage": "https://github.com", 35 | "language": null, 36 | "forks_count": 9, 37 | "stargazers_count": 80, 38 | "watchers_count": 80, 39 | "size": 108, 40 | "master_branch": "master", 41 | "open_issues_count": 0, 42 | "pushed_at": "2011-01-26T19:06:43Z", 43 | "created_at": "2011-01-26T19:01:12Z", 44 | "updated_at": "2011-01-26T19:14:43Z" 45 | } -------------------------------------------------------------------------------- /example_app/service_consumer_app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'json' 3 | require 'rack-flash' 4 | require 'cf-app-utils' 5 | require_relative 'github_repo_helper' 6 | 7 | class ServiceConsumerApp < Sinatra::Base 8 | 9 | # app configuration 10 | enable :sessions 11 | use Rack::Flash, :sweep => true 12 | 13 | #declare the routes used by the app 14 | get "/" do 15 | repo_uris = credentials_of_all_repos.map { |c| c["uri"] } if bindings_exist 16 | 17 | erb :index, locals: {repo_uris: repo_uris, messages: messages} 18 | end 19 | 20 | get "/env" do 21 | content_type "text/plain" 22 | 23 | response_body = "VCAP_SERVICES = \n#{vcap_services}\n\n" 24 | response_body << "VCAP_APPLICATION = \n#{vcap_application}\n\n" 25 | response_body << messages 26 | response_body 27 | end 28 | 29 | post "/create_commit" do 30 | github_repo_helper = GithubRepoHelper.new(credentials_of_all_repos) 31 | repo_uri = params[:repo_uri] 32 | 33 | begin 34 | github_repo_helper.create_commit(repo_uri, application_name) 35 | flash[:notice] = "Successfully pushed commit to #{repo_uri}" 36 | rescue GithubRepoHelper::RepoCredentialsMissingError 37 | flash[:notice] = "Unable to create the commit, repo credentials in VCAP_SERVICES are missing or invalid for: #{repo_uri}" 38 | rescue GithubRepoHelper::CreateCommitError => e 39 | flash[:notice] = "Creating the commit failed. Log contents:\n#{e.message}" 40 | end 41 | 42 | redirect "/" 43 | end 44 | 45 | # helper methods 46 | private 47 | 48 | def messages 49 | result = "" 50 | result << "#{no_bindings_exist_message}" unless bindings_exist 51 | result << "\n\nAfter binding or unbinding any service instances, restart this application with 'cf restart [appname]'." 52 | result 53 | end 54 | 55 | def vcap_services 56 | ENV["VCAP_SERVICES"] 57 | end 58 | 59 | def vcap_application 60 | ENV["VCAP_APPLICATION"] 61 | end 62 | 63 | def application_name 64 | JSON.parse(vcap_application).fetch("application_name") 65 | end 66 | 67 | def bindings_exist 68 | !(credentials_of_all_repos.empty?) 69 | end 70 | 71 | def no_bindings_exist_message 72 | "\n\nYou haven't bound any instances of the #{service_name} service." 73 | end 74 | 75 | def service_name 76 | "github-repo" 77 | end 78 | 79 | def credentials_of_all_repos 80 | CF::App::Credentials.find_all_by_service_label(service_name) 81 | end 82 | end -------------------------------------------------------------------------------- /example_app/test/integration/github_integration_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path '../../test_helper.rb', __FILE__ 2 | require File.expand_path '../integration_test_helper.rb', __FILE__ 3 | 4 | describe "/" do 5 | before do 6 | ensure_env_vars_exist 7 | 8 | @vcap_services_value = < -d '{"scopes": ["repo"], "note": "integration-test-token"}' https://api.github.com/authorizations 63 | def github_access_token 64 | ENV["GITHUB_ACCESS_TOKEN"] 65 | end 66 | 67 | def repo_name 68 | ENV["GITHUB_REPO_NAME"] 69 | end 70 | 71 | def repo_private_key 72 | ENV["GITHUB_REPO_PRIVATE_KEY"] 73 | end 74 | 75 | def repo_ssh_url 76 | "git@github.com:#{github_username}/#{repo_name}.git" 77 | end 78 | 79 | def repo_uri 80 | "https://github.com/#{github_username}/#{repo_name}" 81 | end 82 | 83 | def repo_fullname 84 | "#{github_username}/#{repo_name}" 85 | end 86 | 87 | def github_client 88 | ::Octokit::Client.new(access_token: github_access_token) 89 | end -------------------------------------------------------------------------------- /example_app/test/github_repo_helper_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path '../test_helper.rb', __FILE__ 2 | 3 | include Rack::Test::Methods 4 | 5 | describe GithubRepoHelper do 6 | describe "#create_commit" do 7 | before do 8 | @application_name = "service-consumer-application" 9 | @repo_name = "some-repo" 10 | @repo_uri = "http://fake.github.com/some-user/#{@repo_name}" 11 | @desired_repo_credentials = { 12 | "uri" => @repo_uri, 13 | "name" => @repo_name, 14 | "ssh_url" => "dont-care", 15 | "private_key" => "dont-care" 16 | } 17 | @all_repo_credentials = 18 | [ 19 | @desired_repo_credentials, 20 | { 21 | "uri" => "other-repo-uri", 22 | "name" => "other-repo-name", 23 | "ssh_url" => "dont-care", 24 | "private_key" => "dont-care" 25 | } 26 | ] 27 | 28 | @github_repo_helper = GithubRepoHelper.new(@all_repo_credentials) 29 | @github_repo_helper.stubs(:shell_create_and_push_commit) 30 | end 31 | 32 | describe "when no repo credentials for the uri exist" do 33 | it "raises an error" do 34 | proc { 35 | @github_repo_helper.create_commit("http://uri-not-present-in-list-of-credentials", @application_name) 36 | }.must_raise GithubRepoHelper::RepoCredentialsMissingError 37 | end 38 | end 39 | 40 | describe "when the repo credentials for the uri are found" do 41 | it "creates and pushes the commit to GitHub" do 42 | @github_repo_helper.expects(:shell_create_and_push_commit).with(@desired_repo_credentials, @application_name). 43 | returns({command_status: 0, command_output: "all is well"}) 44 | 45 | @github_repo_helper.create_commit(@repo_uri, @application_name) 46 | end 47 | end 48 | 49 | describe "when any of the fields inside the repo credentials for the uri are missing" do 50 | %w|uri name ssh_url private_key|.each do |key| 51 | it "raises an error when #{key} in credentials is empty" do 52 | @desired_repo_credentials[key] = nil 53 | 54 | proc { 55 | @github_repo_helper.create_commit(@repo_uri, @application_name) 56 | }.must_raise GithubRepoHelper::RepoCredentialsMissingError 57 | end 58 | end 59 | end 60 | 61 | describe "when any shell command fails" do 62 | it "raises an exception with the failure log in the exception message" do 63 | @github_repo_helper.expects(:shell_create_and_push_commit).with(@desired_repo_credentials, @application_name). 64 | returns({command_status: 1, command_output: "some error messages"}) 65 | 66 | exception = proc { 67 | @github_repo_helper.create_commit(@repo_uri, @application_name) 68 | }.must_raise GithubRepoHelper::CreateCommitError 69 | 70 | exception.message.must_equal "some error messages" 71 | end 72 | end 73 | end 74 | end -------------------------------------------------------------------------------- /service_broker/service_broker_app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'json' 3 | require 'yaml' 4 | require_relative 'github_service_helper' 5 | 6 | class ServiceBrokerApp < Sinatra::Base 7 | #configure the Sinatra app 8 | use Rack::Auth::Basic do |username, password| 9 | credentials = self.app_settings.fetch("basic_auth") 10 | username == credentials.fetch("username") and password == credentials.fetch("password") 11 | end 12 | 13 | #declare the routes used by the app 14 | 15 | # CATALOG 16 | get "/v2/catalog" do 17 | content_type :json 18 | 19 | self.class.app_settings.fetch("catalog").to_json 20 | end 21 | 22 | # PROVISION 23 | put "/v2/service_instances/:id" do |id| 24 | content_type :json 25 | 26 | repo_name = repository_name(id) 27 | begin 28 | repo_url = github_service.create_github_repo(repo_name) 29 | status 201 30 | {"dashboard_url" => repo_url}.to_json 31 | rescue GithubServiceHelper::RepoAlreadyExistsError 32 | status 409 33 | {"description" => "The repo #{repo_name} already exists in the GitHub account"}.to_json 34 | rescue GithubServiceHelper::GithubUnreachableError 35 | status 504 36 | {"description" => "GitHub is not reachable"}.to_json 37 | rescue GithubServiceHelper::GithubError => e 38 | status 502 39 | {"description" => e.message}.to_json 40 | end 41 | end 42 | 43 | # BIND 44 | put '/v2/service_instances/:instance_id/service_bindings/:id' do |instance_id, binding_id| 45 | content_type :json 46 | 47 | begin 48 | credentials = github_service.create_github_deploy_key(repo_name: repository_name(instance_id), deploy_key_title: binding_id) 49 | status 201 50 | {"credentials" => credentials}.to_json 51 | rescue GithubServiceHelper::GithubResourceNotFoundError 52 | status 404 53 | {"description" => "GitHub resource not found"}.to_json 54 | rescue GithubServiceHelper::BindingAlreadyExistsError 55 | status 409 56 | {"description" => "The binding #{binding_id} already exists"}.to_json 57 | rescue GithubServiceHelper::GithubUnreachableError 58 | status 504 59 | {"description" => "GitHub is not reachable"}.to_json 60 | rescue GithubServiceHelper::GithubError => e 61 | status 502 62 | {"description" => e.message}.to_json 63 | end 64 | end 65 | 66 | # UNBIND 67 | delete '/v2/service_instances/:instance_id/service_bindings/:id' do |instance_id, binding_id| 68 | content_type :json 69 | 70 | begin 71 | if github_service.remove_github_deploy_key(repo_name: repository_name(instance_id), deploy_key_title: binding_id) 72 | status 200 73 | else 74 | status 410 75 | end 76 | {}.to_json 77 | rescue GithubServiceHelper::GithubResourceNotFoundError 78 | status 410 79 | {}.to_json 80 | rescue GithubServiceHelper::GithubUnreachableError 81 | status 504 82 | {"description" => "GitHub is not reachable"}.to_json 83 | rescue GithubServiceHelper::GithubError => e 84 | status 502 85 | {"description" => e.message}.to_json 86 | end 87 | end 88 | 89 | # UNPROVISION 90 | delete '/v2/service_instances/:instance_id' do |instance_id| 91 | content_type :json 92 | 93 | begin 94 | if github_service.delete_github_repo(repository_name(instance_id)) 95 | status 200 96 | else 97 | status 410 98 | end 99 | {}.to_json 100 | rescue GithubServiceHelper::GithubUnreachableError 101 | status 504 102 | {"description" => "GitHub is not reachable"}.to_json 103 | rescue GithubServiceHelper::GithubError => e 104 | status 502 105 | {"description" => e.message}.to_json 106 | end 107 | end 108 | 109 | #helper methods 110 | private 111 | 112 | def repository_name(id) 113 | "github-service-#{id}" 114 | end 115 | 116 | def self.app_settings 117 | settings_filename = defined?(SETTINGS_FILENAME) ? SETTINGS_FILENAME : 'config/settings.yml' 118 | @app_settings ||= YAML.load_file(settings_filename) 119 | end 120 | 121 | def github_service 122 | github_credentials = self.class.app_settings.fetch("github") 123 | GithubServiceHelper.new(github_credentials.fetch("username"), github_credentials.fetch("access_token")) 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /service_broker/github_service_helper.rb: -------------------------------------------------------------------------------- 1 | require 'octokit' 2 | require 'sshkey' 3 | 4 | class GithubServiceHelper 5 | class RepoAlreadyExistsError < StandardError 6 | end 7 | class BindingAlreadyExistsError < StandardError 8 | end 9 | class GithubResourceNotFoundError < StandardError 10 | end 11 | class GithubUnreachableError < StandardError 12 | end 13 | class GithubError < StandardError 14 | end 15 | 16 | def initialize(login, access_token) 17 | @login = login 18 | @access_token = access_token 19 | end 20 | 21 | def create_github_repo(name) 22 | begin 23 | response = octokit_client.create_repository(name, auto_init: true) 24 | rescue Octokit::Error => e 25 | if e.is_a?(Octokit::UnprocessableEntity) && (e.message.match /name already exists on this account/) 26 | raise GithubServiceHelper::RepoAlreadyExistsError 27 | else 28 | # error due to unknown reason, pass the original error message upstream 29 | raise GithubServiceHelper::GithubError.new("GitHub returned an error - #{e.message}") 30 | end 31 | rescue Faraday::Error::TimeoutError 32 | raise GithubServiceHelper::GithubUnreachableError 33 | end 34 | 35 | repo_https_url(response.full_name) 36 | end 37 | 38 | def delete_github_repo(name) 39 | begin 40 | octokit_client.delete_repository(full_repo_name(name)) 41 | rescue Octokit::Error => e 42 | raise GithubServiceHelper::GithubError.new("GitHub returned an error - #{e.message}") 43 | rescue Faraday::Error::TimeoutError 44 | raise GithubServiceHelper::GithubUnreachableError 45 | end 46 | end 47 | 48 | def create_github_deploy_key(options) 49 | repo_name = options.fetch(:repo_name) 50 | full_repo_name = full_repo_name(repo_name) 51 | deploy_key_title = options.fetch(:deploy_key_title) 52 | 53 | deploy_key_list = get_deploy_keys(full_repo_name) 54 | 55 | raise GithubServiceHelper::BindingAlreadyExistsError if deploy_key_list.map(&:title).include? deploy_key_title 56 | 57 | key_pair = SSHKey.generate 58 | public_key = key_pair.ssh_public_key # get the public key in OpenSSH format 59 | 60 | add_deploy_key(deploy_key_title, full_repo_name, public_key) 61 | 62 | { 63 | name: repo_name, 64 | uri: repo_https_url(full_repo_name), 65 | ssh_url: repo_ssh_url(full_repo_name), 66 | private_key: key_pair.private_key 67 | } 68 | end 69 | 70 | def remove_github_deploy_key(options) 71 | repo_name = options.fetch(:repo_name) 72 | full_repo_name = full_repo_name(repo_name) 73 | deploy_key_title = options.fetch(:deploy_key_title) 74 | 75 | deploy_key_list = get_deploy_keys(full_repo_name) 76 | deploy_key = deploy_key_list.detect { |key| key.title == deploy_key_title } 77 | 78 | return false if deploy_key.nil? 79 | remove_deploy_key(full_repo_name, deploy_key.id) 80 | end 81 | 82 | private 83 | 84 | def get_deploy_keys(full_repo_name) 85 | begin 86 | octokit_client.list_deploy_keys(full_repo_name) 87 | rescue Octokit::NotFound 88 | raise GithubServiceHelper::GithubResourceNotFoundError 89 | rescue Octokit::Error => e 90 | raise GithubServiceHelper::GithubError.new("GitHub returned an error - #{e.message}") 91 | rescue Faraday::Error::TimeoutError 92 | raise GithubServiceHelper::GithubUnreachableError 93 | end 94 | end 95 | 96 | def add_deploy_key(deploy_key_title, full_repo_name, public_key) 97 | begin 98 | octokit_client.add_deploy_key(full_repo_name, deploy_key_title, public_key) 99 | rescue Octokit::Error => e 100 | raise GithubServiceHelper::GithubError.new("GitHub returned an error - #{e.message}") 101 | rescue Faraday::Error::TimeoutError 102 | raise GithubServiceHelper::GithubUnreachableError 103 | end 104 | end 105 | 106 | def remove_deploy_key(full_repo_name, deploy_key_id) 107 | begin 108 | octokit_client.remove_deploy_key(full_repo_name, deploy_key_id) 109 | rescue Octokit::Error => e 110 | raise GithubServiceHelper::GithubError.new("GitHub returned an error - #{e.message}") 111 | rescue Faraday::Error::TimeoutError 112 | raise GithubServiceHelper::GithubUnreachableError 113 | end 114 | end 115 | 116 | def full_repo_name(repo_name) 117 | "#{@login}/#{repo_name}" 118 | end 119 | 120 | def repo_ssh_url(full_repo_name) 121 | "git@github.com:#{full_repo_name}.git" 122 | end 123 | 124 | def repo_https_url(full_repo_name) 125 | "https://github.com/#{full_repo_name}" 126 | end 127 | 128 | def octokit_client 129 | ::Octokit::Client.new(access_token: @access_token) 130 | end 131 | end 132 | 133 | -------------------------------------------------------------------------------- /example_app/github_repo_helper.rb: -------------------------------------------------------------------------------- 1 | class GithubRepoHelper 2 | class CreateCommitError < StandardError 3 | end 4 | class RepoCredentialsMissingError < StandardError 5 | end 6 | 7 | def initialize(all_repo_credentials) 8 | @all_repo_credentials = all_repo_credentials 9 | end 10 | 11 | def create_commit(repo_uri, application_name) 12 | repo_credentials = credentials_for_repo_uri(repo_uri) 13 | if repo_credentials.nil? || !credentials_are_present?(repo_credentials) 14 | raise RepoCredentialsMissingError 15 | end 16 | 17 | create_and_push_result = shell_create_and_push_commit(repo_credentials, application_name) 18 | 19 | raise CreateCommitError.new(create_and_push_result[:command_output]) unless 0 == create_and_push_result[:command_status] 20 | end 21 | 22 | private 23 | 24 | def credentials_for_repo_uri(uri) 25 | # NOTE - per Cloud Controller behavior, there should only be 1 binding, 26 | # hence 1 set of credentials for a service instance 27 | @all_repo_credentials.detect do |credentials| 28 | credentials["uri"] == uri 29 | end 30 | end 31 | 32 | 33 | # This function shells out to issue commands that do the following: 34 | # 35 | # - write ssh private key to file 36 | # - configure git ssh (known hosts, and private key file) 37 | # - clone the repo 38 | # - set git author 39 | # - check out master 40 | # - create empty commit 41 | # - print the commit log 42 | # - push commit to master 43 | # - delete private key 44 | # - delete ssh script 45 | # - delete cloned directory 46 | # 47 | # returns a hash: { 48 | # command_status: , 49 | # command_output: 50 | # } 51 | # command_status is 0 if all commands succeed 52 | # command_status is status code of the failing command if any command fails 53 | def shell_create_and_push_commit(repo_credentials, application_name) 54 | private_key = repo_credentials["private_key"] 55 | repo_name = repo_credentials["name"] 56 | repo_ssh_url = repo_credentials["ssh_url"] 57 | keys_dir = "/tmp/github_keys" 58 | key_file_name = "#{keys_dir}/#{repo_name}.key" 59 | git_ssh_script = "/tmp/#{repo_name}_ssh_script.sh" 60 | known_hosts_file = "/tmp/github_known_hosts" 61 | 62 | # Create directory for storing key file, and set permissions 63 | `if [ ! -d #{keys_dir} ]; then mkdir #{keys_dir}; fi` 64 | `chmod 0700 #{keys_dir}` 65 | 66 | # Store the private key in a file 67 | File.open(key_file_name, "w", 0600) do |f| 68 | f.puts private_key 69 | end 70 | 71 | # Create a unique known hosts file with github's public key, for these purposes: 72 | # 1) since SSH StrictHostKeyChecking is "on" by default, this file prevents SSH from asking the user to 73 | # confirm the github.com host upon the first connection. 74 | # 2) not relying on the default ~/.ssh/known_hosts file 75 | File.open(known_hosts_file, "w", 0700) do |f| 76 | f.puts <&1", 91 | "cd /tmp/#{repo_name} && git config user.name '#{application_name}' 2>&1", 92 | "cd /tmp/#{repo_name} && git commit --allow-empty -m 'auto generated empty commit' 2>&1", 93 | "cd /tmp/#{repo_name} && git log --pretty=format:\"%h%x09%ad%x09%s\" 2>&1", 94 | "cd /tmp/#{repo_name}; GIT_SSH=#{git_ssh_script} git push origin master 2>&1" 95 | ] 96 | 97 | 98 | return_code = 0 99 | output = "" 100 | 101 | commands.each do |command| 102 | output << "\n\n> #{command}\n" 103 | output << `#{command}` 104 | return_code = $? 105 | break if return_code != 0 106 | end 107 | 108 | # Remove the temp files regardless of success or failure 109 | cleanup_commands = [ 110 | "rm #{key_file_name}", 111 | "rm #{git_ssh_script}", 112 | "rm -rf /tmp/#{repo_name}" 113 | ] 114 | 115 | cleanup_commands.each do |command| 116 | output << "\n> #{command}\n" 117 | output << `#{command}` 118 | end 119 | 120 | puts output 121 | {command_status: return_code, command_output: output} 122 | end 123 | 124 | def blank?(value) 125 | value.nil? || value.empty? 126 | end 127 | 128 | def credentials_are_present?(credentials) 129 | !(blank?(credentials["name"]) || 130 | blank?(credentials["uri"]) || 131 | blank?(credentials["ssh_url"]) || 132 | blank?(credentials["private_key"])) 133 | end 134 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | github-service-broker-ruby 2 | ========================== 3 | 4 | ##Build Status 5 | 6 | [![Build Status](https://travis-ci.org/cloudfoundry-samples/github-service-broker-ruby.png?branch=develop)](https://travis-ci.org/cloudfoundry-samples/github-service-broker-ruby) (develop branch) 7 | 8 | [![Build Status](https://travis-ci.org/cloudfoundry-samples/github-service-broker-ruby.png?branch=master)](https://travis-ci.org/cloudfoundry-samples/github-service-broker-ruby) (master) 9 | 10 | 11 | ## Introduction 12 | 13 | A Service Broker is required to integrate any service with a Cloud Foundry instance (for brevity, we'll refer to such an instance simply as "Cloud Foundry") as a [Managed Service](http://docs.cloudfoundry.org/services/). 14 | 15 | This repo contains a service broker written as standalone ruby application (based on [Sinatra](https://github.com/sinatra/sinatra)) that implements the [v2.0 Service Broker API (aka Services API, or Broker API)](http://docs.cloudfoundry.org/services/api.html). 16 | 17 | Generally, a Service Broker can be a standalone application that communicates with one or more services, or can be implemented as a component of a service itself. I.e. if the service itself is a Ruby on Rails application, the code in this repository could be added into the application (either copied in, or added as a Rails engine). 18 | 19 | This Service Broker is intended to provide a simple yet functional, readable example of how Cloud Foundry service brokers operate. Even if you are developing a broker in another language, this should clearly demonstrate the API endpoints you will need to implement. This broker is not meant as an example of best practices for Ruby software design nor does it demonstrate BOSH packaging; for an example of these concepts see [cf-mysql-release](https://github.com/cloudfoundry/cf-mysql-release). 20 | 21 | ## Repo Contents 22 | 23 | This repo contains two applications, a service broker and an example app which can use instances of the service advertised by the broker. The root directory for the service broker application can be found at `github-service-broker-ruby/service_broker`. 24 | 25 | The service broker has been written to be as simple to read as possible. There are three files of note: 26 | 27 | * service_broker_app.rb - This is the service broker. 28 | * github_service_helper.rb - This is how the broker interfaces with GitHub. 29 | * config/settings.yml - The config file contains the service catalog advertised by the broker, credentials used by Cloud Foundry to authenticate with the broker, and credentials used by the broker to authenticate with GitHub. 30 | 31 | ## The GitHub repo service 32 | 33 | In this example, the service provided is the management of repositories inside a single GitHub account owned by the service administrator. 34 | 35 | The Service Broker provides 5 basic functions (see [API documentation](http://docs.cloudfoundry.org/services/api.html)): 36 | 37 | Function | Resulting action | 38 | -------- | :--------------- | 39 | catalog | Advertises the GitHub repo services and the plans offered. 40 | create | Creates a public repository inside the account. This repository can be thought of as a service instance. 41 | bind | Generates a GitHub deploy key which gives write access to the repository, and makes the key and repository URL available to the application bound to the service instance. 42 | unbind | Destroys the deploy key bound to the service instance. 43 | delete | Deletes the service instance (repository). 44 | 45 | The GitHub credentials of the GitHub account administrator should be specified in `settings.yml` if you are deploying your own instance of this broker application. We suggest that you create a dedicated GitHub account solely for the purpose of testing this broker (since it will create and destroy repositories). 46 | 47 | 48 | ## The Service Broker 49 | 50 | ### Configuring the Service Broker 51 | 52 | The file `settings.yml` provides configuration for: 53 | 54 | 1. Basic auth username and password used by Cloud Foundry to authenticate with the service broker 55 | 2. Catalog of services, plans, and associated user-facing metadata 56 | 3. GitHub account credentials used by the broker to authenticate with the Github service 57 | 58 | For this service to be functional, you only need to provide your Github credentials. An access token is used in place of username and password to access your GitHub account. To generate an access token run the following command then copy the value of "token" from the response into the config file. 59 | ``` 60 | curl -u -d '{"scopes": ["repo", "delete_repo"], "note": "CF Service Broker"}' https://api.github.com/authorizations 61 | ``` 62 | 63 | ### Deploying the Service Broker 64 | 65 | This service broker application can be deployed on any environment or hosting service. 66 | 67 | For example, to deploy this broker application to Cloud Foundry 68 | 69 | 1. install the `cf` or `gcf` command line tool 70 | 2. log in as a cloud controller admin using `cf login` or `gcf login` 71 | 3. fork or clone this git repository 72 | 4. add the credentials (username and access token) for the GitHub account in which you want this service broker to provide repository services in `settings.yml`. 73 | 5. edit the Basic Auth username and password in `settings.yml` 74 | 6. `cd` into the application root directory: `github-service-broker-ruby/service_broker/` 75 | 7. run `cf push github-broker` or `gcf push github-broker` to deploy the application to Cloud Foundry 76 | 8. register the service broker with CF (instructions [here](http://docs.cloudfoundry.org/services/managing-service-brokers.html#add-broker)) 77 | 9. make the service plan public (instructions [here](http://docs.cloudfoundry.org/services/access-control.html#enable-access)) 78 | 79 | 80 | ## The GitHub Service Consumer example application 81 | 82 | We've provided an example application you can push to Cloud Foundry, which can be bound to an instance of the github-repo service. After binding the example application to a service instance, Cloud Foundry makes credentials available in the VCAP_SERVICES environment variable. The application can then use the credentials to make commits to the GitHub repository represented by the bound service instance. 83 | 84 | ### Deploying the example app 85 | 86 | 87 | With `cf`: 88 | 89 | ``` 90 | $ cd github-service-broker-ruby/example_app/ 91 | $ cf push github-consumer 92 | $ cf create-service github-repo github-repo-1 --plan public 93 | $ cf bind-service github-repo-1 github-consumer 94 | $ cf services # can be used to verify the binding was created 95 | $ cf restart github-consumer 96 | ``` 97 | 98 | With `gcf`: 99 | 100 | ``` 101 | $ cd github-service-broker-ruby/example_app/ 102 | $ gcf push github-consumer 103 | $ gcf create-service github-repo public github-repo-1 104 | $ gcf bind-service github-consumer github-repo-1 105 | $ gcf services # can be used to verify the binding was created 106 | $ gcf restart github-consumer 107 | ``` 108 | 109 | Point your web browser at `http://github-consumer.` and you should see the example app's interface. If the app has not been bound to a service instance of the github-repo service, you will see a meaningful error. Once the app has been bound and restarted you can click a submit button to make empty commits to the repo represented by the bound service instance. 110 | 111 | ### Testing the example app 112 | 113 | The integration tests verify that the application can make commits and push them to GitHub. 114 | 115 | To run the tests, you'll need to create a test account on GitHub, and store the credentials in environment variables (see `example_app/test/integration/github_integration_test.rb` for details) 116 | 117 | The integration tests can be run by `bundle exec rake integration_test`. 118 | 119 | -------------------------------------------------------------------------------- /example_app/test/service_consumer_app_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path '../test_helper.rb', __FILE__ 2 | 3 | include Rack::Test::Methods 4 | 5 | def app 6 | ServiceConsumerApp.new 7 | end 8 | 9 | def service_name 10 | "github-repo" 11 | end 12 | 13 | describe "GET /" do 14 | def make_request 15 | get "/" 16 | end 17 | 18 | before do 19 | @vcap_services_value = "{}" 20 | ENV.stubs(:[]).with("VCAP_SERVICES").returns(@vcap_services_value) 21 | CF::App::Service.instance_variable_set :@services, nil 22 | end 23 | 24 | it "displays instructions about restarting the app" do 25 | make_request 26 | 27 | last_response.status.must_equal 200 28 | last_response.body.must_match /After binding or unbinding any service instances, restart/ 29 | end 30 | 31 | describe "when no service instances are bound to the app" do 32 | it "displays a message saying that no instances are bound" do 33 | make_request 34 | 35 | last_response.status.must_equal 200 36 | last_response.body.must_match /You haven't bound any instances of the #{service_name} service/ 37 | end 38 | end 39 | 40 | describe "when there are service instances are bound to the app" do 41 | before do 42 | @vcap_services_value = <https://github.com/octocat/hello-world 86 | HTML 87 | last_response.body.must_include expected_link.strip 88 | 89 | expected_link = <https://github.com/octocat/happy-times 91 | HTML 92 | last_response.body.must_include expected_link.strip 93 | end 94 | 95 | end 96 | end 97 | 98 | describe "GET /env" do 99 | def make_request 100 | get "/env" 101 | end 102 | 103 | before do 104 | @vcap_services_value = "{}" 105 | @vcap_application_value = < "topsecret", 258 | "uri" => @repo_uri 259 | }, 260 | { 261 | "password" => "also-very-secret", 262 | "uri" => "uri-of-the-other-repo" 263 | } 264 | ] 265 | 266 | ENV.stubs(:[]).with("VCAP_SERVICES").returns(@vcap_services_value) 267 | ENV.stubs(:[]).with("VCAP_APPLICATION").returns(@vcap_application_value) 268 | CF::App::Service.instance_variable_set :@services, nil 269 | 270 | @fake_github_repo_helper = mock 271 | GithubRepoHelper.expects(:new).with(all_credentials).returns(@fake_github_repo_helper) 272 | @fake_github_repo_helper.stubs(:create_commit) 273 | end 274 | 275 | it "calls GithubRepoHelper#create_commit with the repo URI and application name" do 276 | @fake_github_repo_helper.expects(:create_commit).with(@repo_uri, @application_name) 277 | 278 | make_request 279 | end 280 | 281 | it "redirects to the index page" do 282 | make_request 283 | 284 | last_response.must_be :redirect? 285 | follow_redirect! 286 | last_request.path.must_equal "/" 287 | end 288 | 289 | describe "when creating the commit succeeds" do 290 | it "shows a success message in the flash" do 291 | make_request 292 | 293 | follow_redirect! 294 | flash.wont_be_nil 295 | assert last_response.body.must_include Rack::Utils.escape_html("Successfully pushed commit to #{@repo_uri}") 296 | end 297 | end 298 | 299 | describe "when creating the commit fails" do 300 | describe "because the repo credentials are missing" do 301 | before do 302 | @fake_github_repo_helper.stubs(:create_commit).raises(GithubRepoHelper::RepoCredentialsMissingError) 303 | end 304 | 305 | it "redirects to the index page with the error message in the flash" do 306 | make_request 307 | 308 | follow_redirect! 309 | flash.wont_be_nil 310 | assert last_response.body.must_include Rack::Utils.escape_html("Unable to create the commit, repo credentials in VCAP_SERVICES are missing or invalid for: #{@repo_uri}") 311 | end 312 | end 313 | 314 | describe "for any other reason" do 315 | before do 316 | @fake_github_repo_helper.stubs(:create_commit).raises(GithubRepoHelper::CreateCommitError.new("error message 1\nerror message 2")) 317 | end 318 | 319 | it "redirects to the index page with the error message in the flash" do 320 | make_request 321 | 322 | follow_redirect! 323 | flash.wont_be_nil 324 | assert last_response.body.must_include Rack::Utils.escape_html("Creating the commit failed. Log contents:\nerror message 1\nerror message 2") 325 | end 326 | end 327 | end 328 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /service_broker/test/service_broker_app_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path '../test_helper.rb', __FILE__ 2 | 3 | include Rack::Test::Methods 4 | 5 | def app 6 | ServiceBrokerApp.new 7 | end 8 | 9 | describe "get /v2/catalog" do 10 | def make_request 11 | get "/v2/catalog" 12 | end 13 | 14 | describe "when basic auth credentials are missing" do 15 | before do 16 | make_request 17 | end 18 | 19 | it "returns a 401 unauthorized response" do 20 | assert_equal 401, last_response.status 21 | end 22 | end 23 | 24 | describe "when basic auth credentials are incorrect" do 25 | before do 26 | authorize "admin", "wrong-password" 27 | make_request 28 | end 29 | 30 | it "returns a 401 unauthorized response" do 31 | assert_equal 401, last_response.status 32 | end 33 | end 34 | 35 | describe "when basic auth credentials are correct" do 36 | before do 37 | authorize "admin", "password" 38 | make_request 39 | end 40 | 41 | it "returns a 200 OK response" do 42 | assert_equal 200, last_response.status 43 | end 44 | 45 | it "specifies the content type of the response" do 46 | last_response.header["Content-Type"].must_include("application/json") 47 | end 48 | 49 | it "returns correct keys in JSON" do 50 | response_json = JSON.parse last_response.body 51 | 52 | response_json.keys.must_equal ["services"] 53 | 54 | services = response_json["services"] 55 | assert services.length > 0 56 | 57 | services.each do |service| 58 | assert_operator service.keys.length, :>=, 5 59 | assert service.keys.include? "id" 60 | assert service.keys.include? "name" 61 | assert service.keys.include? "description" 62 | assert service.keys.include? "bindable" 63 | assert service.keys.include? "plans" 64 | 65 | plans = service["plans"] 66 | assert plans.length > 0 67 | plans.each do |plan| 68 | assert_operator plan.keys.length, :>=, 3 69 | assert plan.keys.include? "id" 70 | assert plan.keys.include? "name" 71 | assert plan.keys.include? "description" 72 | end 73 | end 74 | end 75 | 76 | it "contains proper metadata when it is (optionally) provided in settings.yml" do 77 | response_json = JSON.parse last_response.body 78 | 79 | services = response_json["services"] 80 | 81 | services.each do |service| 82 | assert service.keys.include? "metadata" 83 | 84 | plans = service["plans"] 85 | plans.each do |plan| 86 | assert plan.keys.include? "metadata" 87 | plan_costs = plan["metadata"]["costs"] 88 | plan_costs.each do |cost| 89 | assert cost.keys.include? "amount" 90 | assert cost.keys.include? "unit" 91 | assert cost["amount"].keys.include? "usd" 92 | end 93 | end 94 | end 95 | end 96 | end 97 | end 98 | 99 | describe "put /v2/service_instances/:id" do 100 | before do 101 | @id = "1234-5678" 102 | end 103 | 104 | def make_request 105 | put "/v2/service_instances/#{@id}" 106 | end 107 | 108 | describe "when basic auth credentials are missing" do 109 | before do 110 | make_request 111 | end 112 | 113 | it "returns a 401 unauthorized response" do 114 | assert_equal 401, last_response.status 115 | end 116 | end 117 | 118 | describe "when basic auth credentials are incorrect" do 119 | before do 120 | authorize "admin", "wrong-password" 121 | make_request 122 | end 123 | 124 | it "returns a 401 unauthorized response" do 125 | assert_equal 401, last_response.status 126 | end 127 | end 128 | 129 | describe "when basic auth credentials are correct" do 130 | before do 131 | authorize "admin", "password" 132 | 133 | @fake_github_service = mock 134 | GithubServiceHelper.stubs(:new).returns(@fake_github_service) 135 | end 136 | 137 | describe "when repo is successfully created" do 138 | before do 139 | @fake_github_service.stubs(:create_github_repo).with("github-service-1234-5678").returns("http://some.repository.url") 140 | make_request 141 | end 142 | 143 | it "returns '201 Created'" do 144 | assert_equal 201, last_response.status 145 | end 146 | 147 | it "specifies the content type of the response" do 148 | last_response.header["Content-Type"].must_include("application/json") 149 | end 150 | 151 | it "returns json representation of dashboard URL" do 152 | expected_json = { 153 | "dashboard_url" => "http://some.repository.url" 154 | }.to_json 155 | 156 | assert_equal expected_json, last_response.body 157 | end 158 | end 159 | 160 | describe "when the repo already exists" do 161 | before do 162 | @fake_github_service.stubs(:create_github_repo).with("github-service-1234-5678").raises GithubServiceHelper::RepoAlreadyExistsError 163 | make_request 164 | end 165 | 166 | it "returns '409 Conflict'" do 167 | assert_equal 409, last_response.status 168 | end 169 | 170 | it "returns a JSON response explaining the error" do 171 | expected_json = { 172 | "description" => "The repo github-service-#{@id} already exists in the GitHub account" 173 | }.to_json 174 | 175 | assert_equal expected_json, last_response.body 176 | end 177 | end 178 | 179 | describe "when GitHub is not reachable" do 180 | before do 181 | @fake_github_service.stubs(:create_github_repo).with("github-service-1234-5678").raises GithubServiceHelper::GithubUnreachableError 182 | make_request 183 | end 184 | 185 | it "returns 504 Gateway Timeout" do 186 | assert_equal 504, last_response.status 187 | end 188 | 189 | it "returns a JSON response explaining the error" do 190 | expected_json = { 191 | "description" => "GitHub is not reachable" 192 | }.to_json 193 | 194 | assert_equal expected_json, last_response.body 195 | end 196 | end 197 | 198 | describe "when GitHub returns any other error" do 199 | before do 200 | @fake_github_service.stubs(:create_github_repo).with("github-service-1234-5678").raises GithubServiceHelper::GithubError.new("some message") 201 | make_request 202 | end 203 | 204 | it "returns 502 Bad Gateway" do 205 | assert_equal 502, last_response.status 206 | end 207 | 208 | it "returns a JSON response explaining the error" do 209 | expected_json = { 210 | "description" => "some message" 211 | }.to_json 212 | 213 | assert_equal expected_json, last_response.body 214 | end 215 | end 216 | end 217 | end 218 | 219 | describe "put /v2/service_instances/:instance_id/service_bindings/:id" do 220 | before do 221 | @instance_id = "1234" 222 | @binding_id = "5556" 223 | end 224 | 225 | def make_request 226 | put "/v2/service_instances/#{@instance_id}/service_bindings/#{@binding_id}" 227 | end 228 | 229 | describe "when basic auth credentials are missing" do 230 | before do 231 | make_request 232 | end 233 | 234 | it "returns a 401 unauthorized response" do 235 | assert_equal 401, last_response.status 236 | end 237 | end 238 | 239 | describe "when basic auth credentials are incorrect" do 240 | before do 241 | authorize "admin", "wrong-password" 242 | make_request 243 | end 244 | 245 | it "returns a 401 unauthorized response" do 246 | assert_equal 401, last_response.status 247 | end 248 | end 249 | 250 | describe "when basic auth credentials are correct" do 251 | before do 252 | authorize "admin", "password" 253 | 254 | @fake_github_service = mock 255 | GithubServiceHelper.stubs(:new).returns(@fake_github_service) 256 | @fake_github_service.stubs(:create_github_deploy_key) 257 | end 258 | 259 | it "specifies the content type of the response" do 260 | make_request 261 | last_response.header["Content-Type"].must_include("application/json") 262 | end 263 | 264 | describe "when binding succeeds" do 265 | before do 266 | @fake_github_service.expects(:create_github_deploy_key).with(repo_name: "github-service-#{@instance_id}", deploy_key_title: @binding_id). 267 | returns( 268 | { 269 | uri: "http://fake.github.com/some-user/some-repo", 270 | private_key: "private-key" 271 | } 272 | ) 273 | end 274 | 275 | it "returns a 201 Created" do 276 | make_request 277 | assert_equal 201, last_response.status 278 | end 279 | 280 | it "responds with credentials, including the private key and repo url" do 281 | make_request 282 | last_response.body.must_equal({ 283 | credentials: { 284 | uri: "http://fake.github.com/some-user/some-repo", 285 | private_key: "private-key" 286 | } 287 | }.to_json) 288 | end 289 | end 290 | 291 | describe "when the binding with the id already exists" do 292 | before do 293 | @fake_github_service.expects(:create_github_deploy_key).with(repo_name: "github-service-#{@instance_id}", deploy_key_title: @binding_id). 294 | raises GithubServiceHelper::BindingAlreadyExistsError 295 | make_request 296 | end 297 | 298 | it "returns '409 Conflict'" do 299 | assert_equal 409, last_response.status 300 | end 301 | 302 | it "returns a JSON response explaining the error" do 303 | expected_json = { 304 | "description" => "The binding #{@binding_id} already exists" 305 | }.to_json 306 | 307 | assert_equal expected_json, last_response.body 308 | end 309 | end 310 | 311 | describe "when GitHub resource is not found" do 312 | before do 313 | @fake_github_service.expects(:create_github_deploy_key).with(repo_name: "github-service-#{@instance_id}", deploy_key_title: @binding_id). 314 | raises GithubServiceHelper::GithubResourceNotFoundError 315 | make_request 316 | end 317 | 318 | it "returns 404 Not Found" do 319 | assert_equal 404, last_response.status 320 | end 321 | 322 | it "returns a JSON response explaining the error" do 323 | expected_json = { 324 | "description" => "GitHub resource not found" 325 | }.to_json 326 | 327 | assert_equal expected_json, last_response.body 328 | end 329 | end 330 | 331 | describe "when GitHub is not reachable" do 332 | before do 333 | @fake_github_service.expects(:create_github_deploy_key).with(repo_name: "github-service-#{@instance_id}", deploy_key_title: @binding_id). 334 | raises GithubServiceHelper::GithubUnreachableError 335 | make_request 336 | end 337 | 338 | it "returns 504 Gateway Timeout" do 339 | assert_equal 504, last_response.status 340 | end 341 | 342 | it "returns a JSON response explaining the error" do 343 | expected_json = { 344 | "description" => "GitHub is not reachable" 345 | }.to_json 346 | 347 | assert_equal expected_json, last_response.body 348 | end 349 | end 350 | 351 | describe "when GitHub returns any other error" do 352 | before do 353 | @fake_github_service.expects(:create_github_deploy_key).with(repo_name: "github-service-#{@instance_id}", deploy_key_title: @binding_id). 354 | raises GithubServiceHelper::GithubError.new("some message") 355 | make_request 356 | end 357 | 358 | it "returns 502 Bad Gateway" do 359 | assert_equal 502, last_response.status 360 | end 361 | 362 | it "returns a JSON response explaining the error" do 363 | expected_json = { 364 | "description" => "some message" 365 | }.to_json 366 | 367 | assert_equal expected_json, last_response.body 368 | end 369 | end 370 | end 371 | end 372 | 373 | describe "delete /v2/service_instances/:instance_id/service_bindings/:id" do 374 | before do 375 | @instance_id = "1234" 376 | @binding_id = "5556" 377 | end 378 | 379 | def make_request 380 | delete "/v2/service_instances/#{@instance_id}/service_bindings/#{@binding_id}" 381 | end 382 | 383 | describe "when basic auth credentials are missing" do 384 | before do 385 | make_request 386 | end 387 | 388 | it "returns a 401 unauthorized response" do 389 | assert_equal 401, last_response.status 390 | end 391 | end 392 | 393 | describe "when basic auth credentials are incorrect" do 394 | before do 395 | authorize "admin", "wrong-password" 396 | make_request 397 | end 398 | 399 | it "returns a 401 unauthorized response" do 400 | assert_equal 401, last_response.status 401 | end 402 | end 403 | 404 | describe "when basic auth credentials are correct" do 405 | before do 406 | authorize "admin", "password" 407 | 408 | @fake_github_service = mock 409 | GithubServiceHelper.stubs(:new).returns(@fake_github_service) 410 | @fake_github_service.stubs(:remove_github_deploy_key) 411 | end 412 | 413 | it "specifies the content type of the response" do 414 | make_request 415 | last_response.header["Content-Type"].must_include("application/json") 416 | end 417 | 418 | describe "when unbinding succeeds" do 419 | before do 420 | @fake_github_service.expects(:remove_github_deploy_key). 421 | with(repo_name: "github-service-#{@instance_id}", deploy_key_title: @binding_id). 422 | returns(true) 423 | 424 | make_request 425 | end 426 | 427 | it "returns a 200 OK" do 428 | assert_equal 200, last_response.status 429 | end 430 | 431 | it "returns an empty JSON body" do 432 | make_request 433 | last_response.body.must_equal("{}") 434 | end 435 | end 436 | 437 | describe "when unbinding fails" do 438 | describe "because binding id not found" do 439 | before do 440 | @fake_github_service.expects(:remove_github_deploy_key). 441 | with(repo_name: "github-service-#{@instance_id}", deploy_key_title: @binding_id). 442 | returns(false) 443 | 444 | make_request 445 | end 446 | 447 | it "returns a 410 Not found" do 448 | assert_equal 410, last_response.status 449 | end 450 | 451 | it "returns an empty JSON body" do 452 | make_request 453 | last_response.body.must_equal("{}") 454 | end 455 | end 456 | 457 | describe "because GitHub resource is not found" do 458 | before do 459 | @fake_github_service.expects(:remove_github_deploy_key). 460 | with(repo_name: "github-service-#{@instance_id}", deploy_key_title: @binding_id). 461 | raises(GithubServiceHelper::GithubResourceNotFoundError) 462 | 463 | make_request 464 | end 465 | 466 | it "returns a 410 Not found" do 467 | assert_equal 410, last_response.status 468 | end 469 | 470 | it "returns an empty JSON body" do 471 | make_request 472 | last_response.body.must_equal("{}") 473 | end 474 | end 475 | 476 | describe "because GitHub is not reachable" do 477 | before do 478 | @fake_github_service.expects(:remove_github_deploy_key). 479 | with(repo_name: "github-service-#{@instance_id}", deploy_key_title: @binding_id). 480 | raises GithubServiceHelper::GithubUnreachableError 481 | make_request 482 | end 483 | 484 | it "returns 504 Gateway Timeout" do 485 | assert_equal 504, last_response.status 486 | end 487 | 488 | it "returns a JSON response explaining the error" do 489 | expected_json = { 490 | "description" => "GitHub is not reachable" 491 | }.to_json 492 | 493 | assert_equal expected_json, last_response.body 494 | end 495 | end 496 | 497 | describe "because GitHub returns any other error" do 498 | before do 499 | @fake_github_service.expects(:remove_github_deploy_key). 500 | with(repo_name: "github-service-#{@instance_id}", deploy_key_title: @binding_id). 501 | raises GithubServiceHelper::GithubError.new("some message") 502 | make_request 503 | end 504 | 505 | it "returns 502 Bad Gateway" do 506 | assert_equal 502, last_response.status 507 | end 508 | 509 | it "returns a JSON response explaining the error" do 510 | expected_json = { 511 | "description" => "some message" 512 | }.to_json 513 | 514 | assert_equal expected_json, last_response.body 515 | end 516 | end 517 | end 518 | end 519 | end 520 | 521 | describe "delete /v2/service_instances/:instance_id" do 522 | before do 523 | @instance_id = "1234-5678" 524 | end 525 | 526 | def make_request 527 | delete "/v2/service_instances/#{@instance_id}" 528 | end 529 | 530 | describe "when basic auth credentials are missing" do 531 | before do 532 | make_request 533 | end 534 | 535 | it "returns a 401 unauthorized response" do 536 | assert_equal 401, last_response.status 537 | end 538 | end 539 | 540 | describe "when basic auth credentials are incorrect" do 541 | before do 542 | authorize "admin", "wrong-password" 543 | make_request 544 | end 545 | 546 | it "returns a 401 unauthorized response" do 547 | assert_equal 401, last_response.status 548 | end 549 | end 550 | 551 | describe "when basic auth credentials are correct" do 552 | before do 553 | authorize "admin", "password" 554 | 555 | @fake_github_service = mock 556 | GithubServiceHelper.stubs(:new).returns(@fake_github_service) 557 | end 558 | 559 | describe "when repo is successfully deleted" do 560 | before do 561 | @fake_github_service.stubs(:delete_github_repo).with("github-service-#{@instance_id}"). 562 | returns(true) 563 | make_request 564 | end 565 | 566 | it "returns '200 OK'" do 567 | assert_equal 200, last_response.status 568 | end 569 | 570 | it "specifies the content type of the response" do 571 | last_response.header["Content-Type"].must_include("application/json") 572 | end 573 | 574 | it "returns empty JSON" do 575 | assert_equal "{}", last_response.body 576 | end 577 | end 578 | 579 | describe "when repo deletion fails" do 580 | describe "because the specified repo is not found" do 581 | before do 582 | @fake_github_service.stubs(:delete_github_repo).with("github-service-#{@instance_id}"). 583 | returns(false) 584 | make_request 585 | end 586 | 587 | it "returns a 410 Not found" do 588 | assert_equal 410, last_response.status 589 | end 590 | 591 | it "returns an empty JSON body" do 592 | last_response.body.must_equal("{}") 593 | end 594 | end 595 | 596 | describe "because GitHub is not reachable" do 597 | before do 598 | @fake_github_service.stubs(:delete_github_repo).with("github-service-#{@instance_id}"). 599 | raises GithubServiceHelper::GithubUnreachableError 600 | make_request 601 | end 602 | 603 | it "returns 504 Gateway Timeout" do 604 | assert_equal 504, last_response.status 605 | end 606 | 607 | it "returns a JSON response explaining the error" do 608 | expected_json = { 609 | "description" => "GitHub is not reachable" 610 | }.to_json 611 | 612 | assert_equal expected_json, last_response.body 613 | end 614 | end 615 | 616 | describe "because GitHub returns any other error" do 617 | before do 618 | @fake_github_service.stubs(:delete_github_repo).with("github-service-#{@instance_id}"). 619 | raises GithubServiceHelper::GithubError.new("some message") 620 | make_request 621 | end 622 | 623 | it "returns 502 Bad Gateway" do 624 | assert_equal 502, last_response.status 625 | end 626 | 627 | it "returns a JSON response explaining the error" do 628 | expected_json = { 629 | "description" => "some message" 630 | }.to_json 631 | 632 | assert_equal expected_json, last_response.body 633 | end 634 | end 635 | end 636 | end 637 | end 638 | -------------------------------------------------------------------------------- /service_broker/test/github_service_helper_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path '../test_helper.rb', __FILE__ 2 | 3 | include Rack::Test::Methods 4 | 5 | describe GithubServiceHelper do 6 | def stub_successful_deploy_key_list_request(repo_name, empty = false) 7 | response_body = empty ? "[]" : File.read("test/fixtures/list_github_deploy_keys.json") 8 | stub_request(:get, "https://api.github.com/repos/octocat/#{repo_name}/keys"). 9 | with(headers: {"Authorization" => "token access-token"}). 10 | to_return(status: 200, 11 | headers: { 12 | "content-type" => "application/json; charset=utf-8" 13 | }, 14 | body: response_body 15 | ) 16 | end 17 | 18 | describe "#create_repo" do 19 | before do 20 | @repo_name = "Hello-World" 21 | end 22 | 23 | describe "when GitHub successfully creates the repo" do 24 | before do 25 | # stubbing the http request/response to GitHub API 26 | @expected_request = stub_request(:post, "https://api.github.com/user/repos"). 27 | with(headers: {"Authorization" => "token access-token"}, 28 | body: {"auto_init" => true, "name" => @repo_name}.to_json). 29 | to_return(status: 201, 30 | headers: { 31 | "content-type" => "application/json; charset=utf-8" 32 | }, 33 | body: File.read("test/fixtures/create_github_repo_success_response.json") 34 | ) 35 | end 36 | 37 | it "123 makes a request to github" do 38 | GithubServiceHelper.new('octocat', 'access-token').create_github_repo(@repo_name) 39 | assert_requested @expected_request 40 | end 41 | 42 | it "123 returns a repo url" do 43 | response = GithubServiceHelper.new('octocat', 'access-token').create_github_repo(@repo_name) 44 | response.must_equal "https://github.com/octocat/#{@repo_name}" 45 | end 46 | end 47 | 48 | describe "when the repo already exists" do 49 | before do 50 | stub_request(:post, "https://api.github.com/user/repos"). 51 | with(headers: {"Authorization" => "token access-token"}, 52 | :body => {"auto_init" => true, "name" => @repo_name}.to_json). 53 | to_return(status: 422, 54 | headers: { 55 | "content-type" => "application/json; charset=utf-8" 56 | }, 57 | body: File.read("test/fixtures/create_github_repo_failure_already_exists_response.json") 58 | ) 59 | end 60 | 61 | it "raises RepoAlreadyExistsError" do 62 | proc { 63 | GithubServiceHelper.new('octocat', 'access-token').create_github_repo("Hello-World") 64 | }.must_raise GithubServiceHelper::RepoAlreadyExistsError 65 | end 66 | end 67 | 68 | describe "when GitHub returns 422 for any reason other than a repo already existing" do 69 | before do 70 | stub_request(:post, "https://api.github.com/user/repos"). 71 | with(headers: {"Authorization" => "token access-token"}, 72 | :body => {"auto_init" => true, "name" => @repo_name}.to_json). 73 | to_return(status: 422, 74 | headers: { 75 | "content-type" => "application/json; charset=utf-8" 76 | }, 77 | body: { 78 | "message" => "Semantically Invalid" 79 | }.to_json 80 | ) 81 | end 82 | 83 | it "raises CreateRepoError with a message" do 84 | expected_exception = proc { 85 | GithubServiceHelper.new('octocat', 'access-token').create_github_repo("Hello-World") 86 | }.must_raise GithubServiceHelper::GithubError 87 | 88 | expected_exception.message.must_match /GitHub returned an error/ 89 | expected_exception.message.must_match /Semantically Invalid/ 90 | end 91 | end 92 | 93 | describe "when GitHub returns any other error" do 94 | before do 95 | stub_request(:post, "https://api.github.com/user/repos"). 96 | with(headers: {"Authorization" => "token access-token"}, 97 | :body => {"auto_init" => true, "name" => @repo_name}.to_json). 98 | to_return(status: 404, 99 | headers: { 100 | "content-type" => "application/json; charset=utf-8" 101 | }, 102 | body: { 103 | "message" => "Validation Failed" 104 | }.to_json 105 | ) 106 | end 107 | 108 | it "raises CreateRepoError with a message" do 109 | expected_exception = proc { 110 | GithubServiceHelper.new('octocat', 'access-token').create_github_repo("Hello-World") 111 | }.must_raise GithubServiceHelper::GithubError 112 | 113 | expected_exception.message.must_match /GitHub returned an error/ 114 | expected_exception.message.must_match /Validation Failed/ 115 | end 116 | end 117 | 118 | describe "when GitHub is not reachable" do 119 | before do 120 | stub_request(:post, "https://api.github.com/user/repos"). 121 | with(headers: {"Authorization" => "token access-token"}, 122 | :body => {"auto_init" => true, "name" => @repo_name}.to_json). 123 | to_timeout 124 | end 125 | 126 | it "raises GitHubUnreachableError" do 127 | proc { 128 | GithubServiceHelper.new('octocat', 'access-token').create_github_repo("Hello-World") 129 | }.must_raise GithubServiceHelper::GithubUnreachableError 130 | end 131 | end 132 | end 133 | 134 | describe "#create_deploy_key" do 135 | def stub_key_pair_generation 136 | fake_ssh_key_pair = mock 137 | 138 | SSHKey.stubs(:generate).returns(fake_ssh_key_pair) 139 | fake_ssh_key_pair.stubs(:ssh_public_key).returns(@public_key) 140 | fake_ssh_key_pair.stubs(:private_key).returns(@private_key) 141 | end 142 | 143 | before do 144 | @repo_name = "repo-name-same-as-service-instance-id" 145 | @key_title = "key-uuid-same-as-service-binding-id" 146 | @public_key = "ssh-rsa AAA..." 147 | @private_key = "-----BEGIN RSA PRIVATE KEY-----\nZZZ\n-----END RSA PRIVATE KEY-----\n" 148 | end 149 | 150 | describe "when fetching the list of keys fails" do 151 | describe "because GitHub resource (repo or account) is not found" do 152 | before do 153 | stub_request(:get, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 154 | with(headers: {"Authorization" => "token access-token"}). 155 | to_return(status: 404, 156 | headers: { 157 | "content-type" => "application/json; charset=utf-8" 158 | }, 159 | body: File.read("test/fixtures/github_resource_not_found_response.json") 160 | ) 161 | end 162 | 163 | it "raises a GithubResourceNotFound" do 164 | proc { 165 | GithubServiceHelper.new('octocat', 'access-token').create_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 166 | }.must_raise GithubServiceHelper::GithubResourceNotFoundError 167 | end 168 | end 169 | 170 | describe "because GitHub returns any other error" do 171 | before do 172 | stub_request(:get, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 173 | with(headers: {"Authorization" => "token access-token"}). 174 | to_return(status: 422, 175 | headers: { 176 | "content-type" => "application/json; charset=utf-8" 177 | }, 178 | body: File.read("test/fixtures/github_general_error_response.json") 179 | ) 180 | end 181 | 182 | it "raises a GithubError" do 183 | expected_exception = proc { 184 | GithubServiceHelper.new('octocat', 'access-token').create_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 185 | }.must_raise GithubServiceHelper::GithubError 186 | 187 | expected_exception.message.must_match /GitHub returned an error/ 188 | expected_exception.message.must_match /some error message/ 189 | end 190 | end 191 | 192 | describe "because GitHub is not reachable" do 193 | before do 194 | stub_request(:get, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 195 | with(headers: {"Authorization" => "token access-token"}). 196 | to_timeout 197 | end 198 | 199 | it "raises a GithubUnreachableError" do 200 | proc { 201 | GithubServiceHelper.new('octocat', 'access-token').create_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 202 | }.must_raise GithubServiceHelper::GithubUnreachableError 203 | end 204 | end 205 | end 206 | 207 | describe "when fetching the list of keys succeeds" do 208 | describe "when the key is created and added successfully" do 209 | before do 210 | stub_successful_deploy_key_list_request(@repo_name) 211 | 212 | stub_key_pair_generation 213 | 214 | @expected_request = stub_request(:post, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 215 | with(headers: {"Authorization" => "token access-token"}, 216 | :body => { 217 | "title" => @key_title, 218 | "key" => @public_key 219 | }.to_json). 220 | to_return(status: 201, 221 | headers: { 222 | "content-type" => "application/json; charset=utf-8" 223 | }, 224 | body: { 225 | "id" => 1234, 226 | "key" => @public_key, 227 | "url" => "https://api.github.com/user/keys/1234", 228 | "title" => @key_title 229 | }.to_json 230 | ) 231 | end 232 | 233 | it "makes a deploy key creation request to github" do 234 | GithubServiceHelper.new('octocat', 'access-token').create_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 235 | assert_requested @expected_request 236 | end 237 | 238 | it "returns credentials, with repo URI and private key" do 239 | response = GithubServiceHelper.new('octocat', 'access-token').create_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 240 | response.must_equal({ 241 | name: @repo_name, 242 | uri: "https://github.com/octocat/#{@repo_name}", 243 | ssh_url: "git@github.com:octocat/#{@repo_name}.git", 244 | private_key: @private_key 245 | }) 246 | end 247 | 248 | describe "when there are no keys on github" do 249 | it "still succeeds" do 250 | stub_successful_deploy_key_list_request(@repo_name, true) 251 | 252 | response = GithubServiceHelper.new('octocat', 'access-token').create_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 253 | response.must_equal({ 254 | name: @repo_name, 255 | uri: "https://github.com/octocat/#{@repo_name}", 256 | ssh_url: "git@github.com:octocat/#{@repo_name}.git", 257 | private_key: @private_key 258 | }) 259 | end 260 | end 261 | end 262 | 263 | describe "when GitHub returns an error" do 264 | before do 265 | stub_successful_deploy_key_list_request(@repo_name) 266 | 267 | stub_key_pair_generation 268 | 269 | @expected_request = stub_request(:post, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 270 | with(headers: {"Authorization" => "token access-token"}, 271 | :body => { 272 | "title" => @key_title, 273 | "key" => @public_key 274 | }.to_json). 275 | to_return(status: 422, 276 | headers: { 277 | "content-type" => "application/json; charset=utf-8" 278 | }, 279 | body: File.read("test/fixtures/create_github_deploy_key_failure.json") 280 | ) 281 | end 282 | 283 | it "raises a GithubError" do 284 | expected_exception = proc { 285 | GithubServiceHelper.new('octocat', 'access-token').create_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 286 | }.must_raise GithubServiceHelper::GithubError 287 | 288 | expected_exception.message.must_match /GitHub returned an error/ 289 | expected_exception.message.must_match /key is invalid. Ensure you've copied the file correctly/ 290 | end 291 | end 292 | 293 | describe "when GitHub is not reachable" do 294 | before do 295 | stub_successful_deploy_key_list_request(@repo_name) 296 | 297 | stub_key_pair_generation 298 | 299 | stub_request(:post, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 300 | with(:body => { 301 | "title" => @key_title, 302 | "key" => @public_key 303 | }.to_json). 304 | to_timeout 305 | end 306 | 307 | it "raises a GithubUnreachableError" do 308 | proc { 309 | GithubServiceHelper.new('octocat', 'access-token').create_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 310 | }.must_raise GithubServiceHelper::GithubUnreachableError 311 | end 312 | 313 | end 314 | 315 | describe "when a deploy key with a title equal to the requested binding already exists" do 316 | before do 317 | stub_successful_deploy_key_list_request(@repo_name) 318 | end 319 | 320 | it "raises a BindingAlreadyExistsError" do 321 | proc { 322 | GithubServiceHelper.new('octocat', 'access-token').create_github_deploy_key(repo_name: @repo_name, deploy_key_title: "second-key") 323 | }.must_raise GithubServiceHelper::BindingAlreadyExistsError 324 | end 325 | end 326 | end 327 | end 328 | 329 | describe "#remove_deploy_key" do 330 | before do 331 | @repo_name = "whatever-repo" 332 | @key_title = "second-key" 333 | @key_id = 2 334 | end 335 | # 336 | it "requests a list of keys from github" do 337 | @expected_get_keys_request = stub_request(:get, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 338 | with(headers: {"Authorization" => "token access-token"}). 339 | to_return(status: 200, 340 | headers: { 341 | "content-type" => "application/json; charset=utf-8" 342 | }, 343 | body: File.read("test/fixtures/list_github_deploy_keys.json")) 344 | 345 | stub_request(:delete, "https://api.github.com/repos/octocat/#{@repo_name}/keys/#{@key_id}"). 346 | with(headers: {"Authorization" => "token access-token"}). 347 | to_return(status: 204) 348 | 349 | GithubServiceHelper.new('octocat', 'access-token').remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 350 | assert_requested @expected_get_keys_request 351 | end 352 | 353 | describe "when fetching the list of keys fails" do 354 | describe "because github resource not found" do 355 | before do 356 | stub_request(:get, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 357 | with(headers: {"Authorization" => "token access-token"}). 358 | to_return(status: 404, 359 | headers: { 360 | "content-type" => "application/json; charset=utf-8" 361 | }, 362 | body: File.read("test/fixtures/github_resource_not_found_response.json") 363 | ) 364 | end 365 | 366 | it "raises a GithubResourceNotFoundError" do 367 | proc { 368 | GithubServiceHelper.new('octocat', 'access-token'). 369 | remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 370 | }.must_raise GithubServiceHelper::GithubResourceNotFoundError 371 | end 372 | end 373 | 374 | describe "because GitHub is not reachable" do 375 | before do 376 | stub_request(:get, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 377 | with(headers: {"Authorization" => "token access-token"}). 378 | to_timeout 379 | end 380 | 381 | it "raises a GithubUnreachableError" do 382 | proc { 383 | GithubServiceHelper.new('octocat', 'access-token'). 384 | remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 385 | }.must_raise GithubServiceHelper::GithubUnreachableError 386 | end 387 | end 388 | 389 | describe "because GitHub responds with an error" do 390 | before do 391 | stub_request(:get, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 392 | with(headers: {"Authorization" => "token access-token"}). 393 | to_return(status: 422, 394 | headers: { 395 | "content-type" => "application/json; charset=utf-8" 396 | }, 397 | body: File.read("test/fixtures/github_general_error_response.json")) 398 | end 399 | 400 | it "raises a GithubError" do 401 | expected_exception = proc { 402 | GithubServiceHelper.new('octocat', 'access-token'). 403 | remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 404 | }.must_raise GithubServiceHelper::GithubError 405 | 406 | expected_exception.message.must_match /GitHub returned an error/ 407 | expected_exception.message.must_match /some error message/ 408 | end 409 | end 410 | end 411 | 412 | describe "when fetching the list of keys succeeds" do 413 | describe "when there are no keys in the list" do 414 | before do 415 | stub_request(:get, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 416 | with(headers: {"Authorization" => "token access-token"}). 417 | to_return(status: 200, 418 | headers: { 419 | "content-type" => "application/json; charset=utf-8" 420 | }, 421 | body: "[]") 422 | end 423 | 424 | it "returns false" do 425 | result = GithubServiceHelper.new('octocat', 'access-token'). 426 | remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: "does-not-exist") 427 | 428 | assert_equal false, result 429 | end 430 | end 431 | 432 | describe "when the key is not found on github" do 433 | before do 434 | stub_request(:get, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 435 | with(headers: {"Authorization" => "token access-token"}). 436 | to_return(status: 200, 437 | headers: { 438 | "content-type" => "application/json; charset=utf-8", 439 | 'Authorization' => 'token access-token' 440 | }, 441 | body: File.read("test/fixtures/list_github_deploy_keys.json") 442 | ) 443 | end 444 | 445 | it "returns false" do 446 | result = GithubServiceHelper.new('octocat', 'access-token'). 447 | remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: "does-not-exist") 448 | assert_equal false, result 449 | end 450 | end 451 | 452 | describe "when the requested key exists on github" do 453 | before do 454 | stub_request(:get, "https://api.github.com/repos/octocat/#{@repo_name}/keys"). 455 | with(headers: {"Authorization" => "token access-token"}). 456 | to_return(status: 200, 457 | headers: { 458 | "content-type" => "application/json; charset=utf-8" 459 | }, 460 | body: File.read("test/fixtures/list_github_deploy_keys.json") 461 | ) 462 | 463 | @expected_request = stub_request(:delete, "https://api.github.com/repos/octocat/#{@repo_name}/keys/#{@key_id}"). 464 | with(headers: {"Authorization" => "token access-token"}) 465 | end 466 | 467 | it "requests github to remove the deploy key" do 468 | GithubServiceHelper.new('octocat', 'access-token').remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 469 | assert_requested @expected_request 470 | end 471 | 472 | describe "when removal succeeds" do 473 | before do 474 | stub_request(:delete, "https://api.github.com/repos/octocat/#{@repo_name}/keys/#{@key_id}"). 475 | with(headers: {"Authorization" => "token access-token"}). 476 | to_return(status: 204) 477 | end 478 | 479 | it "returns true" do 480 | result = GithubServiceHelper.new('octocat', 'access-token'). 481 | remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 482 | assert_equal true, result 483 | end 484 | end 485 | 486 | describe "when removal fails" do 487 | describe "because GitHub cannot find the resource" do 488 | before do 489 | stub_request(:delete, "https://api.github.com/repos/octocat/#{@repo_name}/keys/#{@key_id}"). 490 | with(headers: {"Authorization" => "token access-token"}). 491 | to_return(status: 404) 492 | end 493 | 494 | it "returns false" do 495 | result = GithubServiceHelper.new('octocat', 'access-token'). 496 | remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 497 | assert_equal false, result 498 | end 499 | end 500 | 501 | describe "because GitHub returns an error" do 502 | before do 503 | stub_request(:delete, "https://api.github.com/repos/octocat/#{@repo_name}/keys/#{@key_id}"). 504 | with(headers: {"Authorization" => "token access-token"}). 505 | to_return(status: 422, 506 | headers: { 507 | "content-type" => "application/json; charset=utf-8" 508 | }, 509 | body: File.read("test/fixtures/github_general_error_response.json")) 510 | end 511 | 512 | it "raises a GithubError" do 513 | expected_exception = proc { 514 | GithubServiceHelper.new('octocat', 'access-token'). 515 | remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 516 | }.must_raise GithubServiceHelper::GithubError 517 | 518 | expected_exception.message.must_match /GitHub returned an error/ 519 | expected_exception.message.must_match /some error message/ 520 | end 521 | end 522 | 523 | describe "because GitHub is not reachable" do 524 | before do 525 | stub_request(:delete, "https://api.github.com/repos/octocat/#{@repo_name}/keys/#{@key_id}"). 526 | with(headers: {"Authorization" => "token access-token"}). 527 | to_timeout 528 | end 529 | 530 | it "raises a GithubUnreachableError" do 531 | proc { 532 | GithubServiceHelper.new('octocat', 'access-token').remove_github_deploy_key(repo_name: @repo_name, deploy_key_title: @key_title) 533 | }.must_raise GithubServiceHelper::GithubUnreachableError 534 | end 535 | end 536 | end 537 | end 538 | end 539 | end 540 | 541 | describe "#delete_repo" do 542 | before do 543 | @repo_name = "whatever-repo" 544 | @expected_request = stub_request(:delete, "https://api.github.com/repos/octocat/#{@repo_name}"). 545 | with(headers: {"Authorization" => "token access-token"}) 546 | end 547 | 548 | it "makes a repo deletion request to github" do 549 | GithubServiceHelper.new('octocat', 'access-token').delete_github_repo(@repo_name) 550 | assert_requested @expected_request 551 | end 552 | 553 | describe "when deletion succeeds" do 554 | before do 555 | stub_request(:delete, "https://api.github.com/repos/octocat/#{@repo_name}"). 556 | with(headers: {"Authorization" => "token access-token"}). 557 | to_return(status: 204) 558 | end 559 | 560 | it "returns true" do 561 | result = GithubServiceHelper.new('octocat', 'access-token').delete_github_repo(@repo_name) 562 | assert_equal true, result 563 | end 564 | end 565 | 566 | describe "when the repo does not exist" do 567 | before do 568 | stub_request(:delete, "https://api.github.com/repos/octocat/repo-that-does-not-exist"). 569 | with(headers: {"Authorization" => "token access-token"}). 570 | to_return(status: 404, 571 | headers: { 572 | "content-type" => "application/json; charset=utf-8" 573 | }, 574 | body: File.read("test/fixtures/github_resource_not_found_response.json")) 575 | end 576 | 577 | it "returns false" do 578 | result = GithubServiceHelper.new('octocat', 'access-token').delete_github_repo("repo-that-does-not-exist") 579 | assert_equal false, result 580 | end 581 | end 582 | 583 | describe "when GitHub returns an error" do 584 | before do 585 | stub_request(:delete, "https://api.github.com/repos/octocat/#{@repo_name}"). 586 | with(headers: {"Authorization" => "token access-token"}). 587 | to_return(status: 422, 588 | headers: { 589 | "content-type" => "application/json; charset=utf-8" 590 | }, 591 | body: File.read("test/fixtures/github_general_error_response.json")) 592 | end 593 | 594 | it "raises a GithubError" do 595 | expected_exception = proc { 596 | GithubServiceHelper.new('octocat', 'access-token').delete_github_repo(@repo_name) 597 | }.must_raise GithubServiceHelper::GithubError 598 | 599 | expected_exception.message.must_match /GitHub returned an error/ 600 | expected_exception.message.must_match /some error message/ 601 | end 602 | end 603 | 604 | describe "when GitHub is not reachable" do 605 | before do 606 | stub_successful_deploy_key_list_request(@repo_name) 607 | 608 | stub_request(:delete, "https://api.github.com/repos/octocat/#{@repo_name}"). 609 | with(headers: {"Authorization" => "token access-token"}). 610 | to_timeout 611 | end 612 | 613 | it "raises a GithubUnreachableError" do 614 | proc { 615 | GithubServiceHelper.new('octocat', 'access-token').delete_github_repo(@repo_name) 616 | }.must_raise GithubServiceHelper::GithubUnreachableError 617 | end 618 | end 619 | end 620 | end 621 | --------------------------------------------------------------------------------