├── 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 | | Repo URL |
21 | |
22 |
23 | <% repo_uris.each do |uri| %>
24 |
25 | | <%= uri %> |
26 |
27 |
31 | |
32 |
33 | <% end %>
34 |
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 | [](https://travis-ci.org/cloudfoundry-samples/github-service-broker-ruby) (develop branch)
7 |
8 | [](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 |
--------------------------------------------------------------------------------