underway 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/Rakefile +8 -0
- data/config.json.example +8 -0
- data/example/Gemfile +6 -0
- data/example/app.rb +30 -0
- data/example/config.json.example +8 -0
- data/lib/underway.rb +11 -0
- data/lib/underway/api.rb +82 -0
- data/lib/underway/database.rb +31 -0
- data/lib/underway/logger.rb +7 -0
- data/lib/underway/sawyer_to_json.rb +18 -0
- data/lib/underway/settings.rb +101 -0
- data/lib/underway/sinatra.rb +24 -0
- data/lib/underway/sinatra/app_info.rb +81 -0
- data/lib/underway/token_cache.rb +26 -0
- data/lib/underway/version.rb +3 -0
- data/test/lib/sawyer_to_json_test.rb +30 -0
- data/test/lib/token_cache_test.rb +66 -0
- data/test/sequel_helper.rb +14 -0
- data/test/test_helper.rb +7 -0
- data/underway.gemspec +32 -0
- metadata +213 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0725b18ff63695654ff86cd63ba3d4669971a8ca
|
4
|
+
data.tar.gz: 2804c0a2916e847b2a7a53739c1568f94e7fcd99
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 19914c5e32911685356e467ea190e73ac62222b939d9234c6e9180e3483d8433b1ddea0bf59fffd1990097eb5c326b5bc26d08c50a8d2fcd55179b6ded028dec
|
7
|
+
data.tar.gz: 1601a1f138f9dcf6ea6a709a2d272165deaff6fa1cb4a98b0c5327111a06ab912f62a391542114b6f4b9d5070d445b290577448bb0d34e109a5633d3ec909447
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
config.json
|
2
|
+
config.json.localhost
|
3
|
+
config.json.prod*
|
4
|
+
*.pem
|
5
|
+
*.db
|
6
|
+
*.gem
|
7
|
+
*.rbc
|
8
|
+
.bundle
|
9
|
+
.config
|
10
|
+
.yardoc
|
11
|
+
Gemfile.lock
|
12
|
+
InstalledFiles
|
13
|
+
_yardoc
|
14
|
+
coverage
|
15
|
+
doc/
|
16
|
+
lib/bundler/man
|
17
|
+
pkg
|
18
|
+
rdoc
|
19
|
+
test/tmp
|
20
|
+
test/version_tmp
|
21
|
+
tmp
|
22
|
+
.ruby-*
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2018 James Martin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Underway
|
2
|
+
|
3
|
+
Underway is a Ruby gem that helps developers quickly prototype [GitHub
|
4
|
+
Apps](https://developer.github.com/apps/).
|
5
|
+
|
6
|
+
Underway consists of some convenience wrappers for the GitHub REST API, with a
|
7
|
+
particular focus on generating credentials for accessing installation resources
|
8
|
+
associated with a GitHub App. Access tokens are cached using Sqlite3 for
|
9
|
+
convenience.
|
10
|
+
|
11
|
+
If you like rapid prototyping with [Sinatra](http://sinatrarb.com) you can use
|
12
|
+
the included Sinatra routes, which allow you to quickly get access to a JWT or
|
13
|
+
access token for your App and its installations. Starting with a Sinatra
|
14
|
+
application is a fast way to build a GitHub App prototype and explore how
|
15
|
+
GitHub Apps work with the GitHub REST API.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem "underway", "~> 1.0"
|
23
|
+
```
|
24
|
+
|
25
|
+
And then run:
|
26
|
+
|
27
|
+
```
|
28
|
+
bundle
|
29
|
+
```
|
30
|
+
|
31
|
+
Or install it globally with:
|
32
|
+
|
33
|
+
```
|
34
|
+
gem install underway
|
35
|
+
```
|
36
|
+
|
37
|
+
## Configuration
|
38
|
+
|
39
|
+
First, follow the documentation to [create a GitHub
|
40
|
+
App](https://developer.github.com/apps/building-github-apps/creating-a-github-app/).
|
41
|
+
|
42
|
+
When you're done creating a new App you should have:
|
43
|
+
|
44
|
+
- Your GitHub App's ID (an integer)
|
45
|
+
- A private key file (.pem)
|
46
|
+
- A webhook secret (optional)
|
47
|
+
|
48
|
+
Make a copy of the [config.json.example]() file in a location readable by your
|
49
|
+
application and edit the file to match your GitHub App's settings.
|
50
|
+
|
51
|
+
At its core, Underway is just a Ruby library and can be used in virtually any
|
52
|
+
application. To get started quickly and test out your new GitHub App you might
|
53
|
+
want to use the included [Sinatra](http://sinatrarb.com) routes. A complete
|
54
|
+
[example Sinatra
|
55
|
+
application](https://github.com/jamesmartin/underway/blob/master/example/app.rb)
|
56
|
+
is included with Underway and shows how to configure the gem and include the
|
57
|
+
informational routes.
|
58
|
+
|
59
|
+
|
60
|
+
If you would prefer to configure Underway manually, or are not building a
|
61
|
+
Sinatra application, do something like this:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
require "underway"
|
65
|
+
|
66
|
+
# This assumes that your configuration file and private key file is located in
|
67
|
+
# the same directory as the current ruby file.
|
68
|
+
|
69
|
+
Underway::Settings.configure do |config|
|
70
|
+
config.app_root = __FILE__
|
71
|
+
config.config_filename = "config.json"
|
72
|
+
end
|
73
|
+
```
|
data/Rakefile
ADDED
data/config.json.example
ADDED
data/example/Gemfile
ADDED
data/example/app.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "sinatra"
|
2
|
+
require "underway"
|
3
|
+
|
4
|
+
configure do
|
5
|
+
Underway::Settings.configure do |config|
|
6
|
+
config.app_root = __FILE__
|
7
|
+
config.config_filename = "config.json"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
include Sinatra::Underway
|
12
|
+
include Sinatra::Underway::AppInfo
|
13
|
+
|
14
|
+
get "/" do
|
15
|
+
erb <<~EOS
|
16
|
+
<a href="/info">Underway App Information</a>
|
17
|
+
EOS
|
18
|
+
end
|
19
|
+
|
20
|
+
post "/user_authz" do
|
21
|
+
debug_route(request)
|
22
|
+
end
|
23
|
+
|
24
|
+
get "/setup" do
|
25
|
+
debug_route(request)
|
26
|
+
end
|
27
|
+
|
28
|
+
post "/hook" do
|
29
|
+
debug_route(request)
|
30
|
+
end
|
data/lib/underway.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "underway/api"
|
2
|
+
require "underway/database"
|
3
|
+
require "underway/logger"
|
4
|
+
require "underway/sawyer_to_json"
|
5
|
+
require "underway/settings"
|
6
|
+
require "underway/sinatra"
|
7
|
+
require "underway/token_cache"
|
8
|
+
require "underway/version"
|
9
|
+
|
10
|
+
module Underway
|
11
|
+
end
|
data/lib/underway/api.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require "jwt"
|
2
|
+
require "octokit"
|
3
|
+
|
4
|
+
module Underway
|
5
|
+
class Api
|
6
|
+
# Returns a Sawyer::Resource or PORO from the GitHub REST API
|
7
|
+
def self.invoke(route, headers: {}, method: :get)
|
8
|
+
debug_octokit! if verbose_logging?
|
9
|
+
|
10
|
+
Octokit.api_endpoint = Underway::Settings.config.raw["github_api_host"]
|
11
|
+
|
12
|
+
if !headers[:authorization] && !headers["Authorization"]
|
13
|
+
Octokit.bearer_token = generate_jwt
|
14
|
+
end
|
15
|
+
|
16
|
+
final_headers = {
|
17
|
+
accept: "application/vnd.github.machine-man-preview+json",
|
18
|
+
headers: headers
|
19
|
+
}
|
20
|
+
|
21
|
+
begin
|
22
|
+
case method
|
23
|
+
when :post then Octokit.post(route, final_headers)
|
24
|
+
else Octokit.get(route, final_headers)
|
25
|
+
end
|
26
|
+
rescue Octokit::Error => e
|
27
|
+
{ error: e.to_s }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.generate_jwt
|
32
|
+
payload = {
|
33
|
+
# Issued at time:
|
34
|
+
iat: Time.now.to_i,
|
35
|
+
# JWT expiration time (10 minute maximum)
|
36
|
+
exp: Time.now.to_i + (10 * 60),
|
37
|
+
# GitHub Apps identifier
|
38
|
+
iss: Underway::Settings.config.app_issuer
|
39
|
+
}
|
40
|
+
|
41
|
+
JWT.encode(payload, Underway::Settings.config.private_key, "RS256")
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns a valid auth token for the installation
|
45
|
+
def self.installation_token(id:)
|
46
|
+
if token = Underway::Settings.config.token_cache.lookup_installation_auth_token(id: id)
|
47
|
+
log("token cache: hit")
|
48
|
+
return token
|
49
|
+
else
|
50
|
+
log("token cache: miss")
|
51
|
+
res = invoke(
|
52
|
+
"/app/installations/#{id}/access_tokens",
|
53
|
+
method: :post
|
54
|
+
)
|
55
|
+
|
56
|
+
if error = res[:error]
|
57
|
+
raise ArgumentError.new(error)
|
58
|
+
end
|
59
|
+
|
60
|
+
token = res.token
|
61
|
+
expires_at = res.expires_at.to_s
|
62
|
+
Underway::Settings.config.token_cache.store_installation_auth_token(id: id, token: token, expires_at: expires_at)
|
63
|
+
token
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.debug_octokit!
|
68
|
+
stack = Faraday::RackBuilder.new do |builder|
|
69
|
+
builder.use Octokit::Middleware::FollowRedirects
|
70
|
+
builder.use Octokit::Response::RaiseError
|
71
|
+
builder.use Octokit::Response::FeedParser
|
72
|
+
builder.response :logger
|
73
|
+
builder.adapter Faraday.default_adapter
|
74
|
+
end
|
75
|
+
Octokit.middleware = stack
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.verbose_logging?
|
79
|
+
!!Underway::Settings.config.verbose_logging
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "sqlite3"
|
2
|
+
require "sequel"
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module Underway
|
6
|
+
class DB
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
@@db = nil
|
10
|
+
|
11
|
+
def self.configure(database_url)
|
12
|
+
@@db = Sequel.connect(database_url)
|
13
|
+
|
14
|
+
Sequel.default_timezone = :utc
|
15
|
+
|
16
|
+
# TODO: extract to schema migration
|
17
|
+
@@db.create_table?(:cached_tokens) do
|
18
|
+
primary_key :id
|
19
|
+
Fixnum :installation_id, null: false
|
20
|
+
String :token, null: false
|
21
|
+
DateTime :expires_at, null: false
|
22
|
+
|
23
|
+
index [:installation_id, :expires_at]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def database
|
28
|
+
@@db
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "json"
|
2
|
+
require "sawyer"
|
3
|
+
|
4
|
+
module Underway
|
5
|
+
class SawyerToJson
|
6
|
+
def self.convert(object)
|
7
|
+
JSON.generate(unwrap(object))
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.unwrap(object)
|
11
|
+
case object
|
12
|
+
when Array then object.map { |o| unwrap(o) }
|
13
|
+
when Sawyer::Resource then object.to_hash
|
14
|
+
else object
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Underway
|
5
|
+
class Settings
|
6
|
+
class Configuration
|
7
|
+
attr_reader :config, :logger
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@logger = Underway::Logger.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def load!
|
14
|
+
@config = JSON.parse(
|
15
|
+
@app_root.join(@config_filename).read
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO: deprecate
|
20
|
+
def raw
|
21
|
+
@config
|
22
|
+
end
|
23
|
+
|
24
|
+
def app_root=(directory)
|
25
|
+
@app_root = Pathname.new(directory).dirname
|
26
|
+
end
|
27
|
+
|
28
|
+
def config_filename=(filename)
|
29
|
+
@config_filename = filename
|
30
|
+
end
|
31
|
+
|
32
|
+
def logger=(logger)
|
33
|
+
@logger = logger
|
34
|
+
end
|
35
|
+
|
36
|
+
def db
|
37
|
+
@db ||=
|
38
|
+
begin
|
39
|
+
Underway::DB.configure(config["database_url"])
|
40
|
+
Underway::DB.instance.database
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# The Integration ID
|
45
|
+
# From "About -> ID" at github.com/settings/apps/<app-name>
|
46
|
+
def app_issuer
|
47
|
+
@app_issuer ||= config["app_id"]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Integration webhook secret (for validating that webhooks come from GitHub)
|
51
|
+
def webhook_secret
|
52
|
+
@webhook_secret ||= config["webhook_secret"]
|
53
|
+
end
|
54
|
+
|
55
|
+
def private_key_filename
|
56
|
+
@app_root.join(config["private_key_filename"])
|
57
|
+
end
|
58
|
+
|
59
|
+
# PEM file for request signing (PKCS#1 RSAPrivateKey format)
|
60
|
+
# (Download from github.com/settings/apps/<app-name> "Private key")
|
61
|
+
def private_pem
|
62
|
+
@private_pem ||= File.read(private_key_filename)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Private Key for the App, generated based on the PEM file
|
66
|
+
def private_key
|
67
|
+
@private_key ||= OpenSSL::PKey::RSA.new(private_pem)
|
68
|
+
end
|
69
|
+
|
70
|
+
def verbose_logging
|
71
|
+
@verbose ||= config["verbose_logging"]
|
72
|
+
end
|
73
|
+
|
74
|
+
def token_cache
|
75
|
+
@token_cache ||= Underway::TokenCache.new(db)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
@configuration = Configuration.new
|
80
|
+
|
81
|
+
class << self
|
82
|
+
attr_reader :configuration
|
83
|
+
|
84
|
+
def configure
|
85
|
+
if block_given?
|
86
|
+
yield configuration
|
87
|
+
else
|
88
|
+
raise ArgumentError.new("Please set configuration by passing a block")
|
89
|
+
end
|
90
|
+
|
91
|
+
configuration.load!
|
92
|
+
end
|
93
|
+
|
94
|
+
# TODO: remove me
|
95
|
+
def config
|
96
|
+
configuration
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "underway/sinatra/app_info"
|
2
|
+
|
3
|
+
# Repopen Sinatra to add helpers to the app
|
4
|
+
module Sinatra
|
5
|
+
module Underway
|
6
|
+
def debug_route(request)
|
7
|
+
log(request.inspect)
|
8
|
+
end
|
9
|
+
|
10
|
+
def verbose_logging?
|
11
|
+
!!::Underway::Settings.config.verbose_logging
|
12
|
+
end
|
13
|
+
|
14
|
+
def log(message)
|
15
|
+
if verbose_logging?
|
16
|
+
::Underway::Settings.config.logger.info(message)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def gh_api(*args)
|
21
|
+
::Underway::Api.invoke(*args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
|
3
|
+
# Include this module in your Sinatra app to get access to these helpful
|
4
|
+
# routes:
|
5
|
+
#
|
6
|
+
# /info/app => information about this App
|
7
|
+
# /info/app/installations => a list of all installations of this App
|
8
|
+
# /info/app/installations/:id => information about the installation
|
9
|
+
# /info/app/installations/:id/repositories => the repositories this
|
10
|
+
# installation has access to
|
11
|
+
# /info/app/installations/:id/access_token => a valid access token for the
|
12
|
+
# installation
|
13
|
+
|
14
|
+
module Sinatra
|
15
|
+
module Underway
|
16
|
+
module AppInfo
|
17
|
+
def self.registered(app)
|
18
|
+
app.get "/info" do
|
19
|
+
erb <<~EOS
|
20
|
+
<h1>Underway</h1>
|
21
|
+
<h2>Interesting routes:</h2>
|
22
|
+
<pre>
|
23
|
+
<li>/info => This page</li>
|
24
|
+
<li><a href="/info/app">/info/app</a> => Information about the configured GitHub App</li>
|
25
|
+
<li><a href="/info/app/jwt">/info/app/jwt</a> => Generates a JWT for authentication as this App</li>
|
26
|
+
<li><a href="/info/app/installations">/info/app/installations</a> => A list of installations associated with this App</li>
|
27
|
+
<li><a href="/info/app/installations/1">/info/app/installations/:id</a> => Information about the given installation of this App</li>
|
28
|
+
<li><a href="/info/app/installations/1/access_token">/info/app/installations/:id/access_token</a> => A valid access token for accessing the given installation as this App</li>
|
29
|
+
<li><a href="/info/app/installations/1/repositories">/info/app/installations/:id/repositories</a> => A list of all repositories accessible to the installation of this App</li>
|
30
|
+
</pre>
|
31
|
+
<h2>Private PEM file</h2>
|
32
|
+
<pre>
|
33
|
+
#{::Underway::Settings.config.private_key_filename}
|
34
|
+
</pre>
|
35
|
+
EOS
|
36
|
+
end
|
37
|
+
|
38
|
+
app.get "/info/app/jwt" do
|
39
|
+
content_type :json
|
40
|
+
::Underway::Api.generate_jwt
|
41
|
+
end
|
42
|
+
|
43
|
+
app.get "/info/app" do
|
44
|
+
content_type :json
|
45
|
+
::Underway::SawyerToJson.convert(gh_api("/app"))
|
46
|
+
end
|
47
|
+
|
48
|
+
app.get "/info/app/installations" do
|
49
|
+
content_type :json
|
50
|
+
::Underway::SawyerToJson.convert(gh_api("/app/installations"))
|
51
|
+
end
|
52
|
+
|
53
|
+
app.get "/info/app/installations/:installation_id" do
|
54
|
+
content_type :json
|
55
|
+
::Underway::SawyerToJson.convert(gh_api("/app/installations/#{params["installation_id"]}"))
|
56
|
+
end
|
57
|
+
|
58
|
+
app.get "/info/app/installations/:installation_id/access_token" do
|
59
|
+
content_type :json
|
60
|
+
::Underway::SawyerToJson.convert(
|
61
|
+
gh_api(
|
62
|
+
"/app/installations/#{params["installation_id"]}/access_tokens",
|
63
|
+
method: :post
|
64
|
+
)
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
app.get "/info/app/installations/:installation_id/repositories" do
|
69
|
+
content_type :json
|
70
|
+
::Underway::SawyerToJson.convert(
|
71
|
+
gh_api(
|
72
|
+
"/installation/repositories",
|
73
|
+
headers: { authorization: "token #{::Underway::Api.installation_token(id: params[:installation_id])}" }
|
74
|
+
)
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
register Underway::AppInfo
|
81
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Underway
|
2
|
+
class TokenCache
|
3
|
+
attr_accessor :db
|
4
|
+
|
5
|
+
def initialize(database)
|
6
|
+
@db = database
|
7
|
+
end
|
8
|
+
|
9
|
+
def lookup_installation_auth_token(id:)
|
10
|
+
results = db[:cached_tokens].where(installation_id: id)
|
11
|
+
.where{expires_at >= DateTime.now.new_offset(0)} # Force UTC Timezone
|
12
|
+
.reverse(:expires_at)
|
13
|
+
if results.any?
|
14
|
+
results.first[:token]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def store_installation_auth_token(id:, token:, expires_at:)
|
19
|
+
db[:cached_tokens].insert(
|
20
|
+
installation_id: id,
|
21
|
+
token: token,
|
22
|
+
expires_at: DateTime.parse(expires_at)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
|
3
|
+
class SawyerToJsonTest < SequelTestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_can_convert_an_empty_array
|
9
|
+
assert_equal "[]", Underway::SawyerToJson.convert([])
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_can_convert_an_array_with_nested_hash
|
13
|
+
obj = [ { foo: "bar" } ]
|
14
|
+
expected = "[{\"foo\":\"bar\"}]"
|
15
|
+
actual = Underway::SawyerToJson.convert(obj)
|
16
|
+
|
17
|
+
assert_equal expected, actual
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_can_convert_a_nested_sawyer_object
|
21
|
+
agent = Sawyer::Agent.new("/irrelevant")
|
22
|
+
data = "{\"foo\":\"bar\"}"
|
23
|
+
resource = Sawyer::Resource.new(agent, agent.decode_body(data))
|
24
|
+
obj = [ resource ]
|
25
|
+
expected = "[{\"foo\":\"bar\"}]"
|
26
|
+
actual = Underway::SawyerToJson.convert(obj)
|
27
|
+
|
28
|
+
assert_equal expected, actual
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
|
3
|
+
class TokenCacheTest < SequelTestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@cache = Underway::TokenCache.new(Underway::DB.instance.database)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_can_store_and_retrieve_a_token
|
10
|
+
Timecop.freeze(DateTime.parse("2018-02-12T09:00:00+00:00")) do
|
11
|
+
@cache.store_installation_auth_token(
|
12
|
+
id: 1,
|
13
|
+
token: "some-token",
|
14
|
+
expires_at: "2018-02-12T10:00:00Z"
|
15
|
+
)
|
16
|
+
|
17
|
+
assert_equal "some-token", @cache.lookup_installation_auth_token(id: 1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_retrieves_the_newest_token
|
22
|
+
Timecop.freeze(DateTime.parse("2018-02-12T09:00:00+00:00")) do
|
23
|
+
@cache.store_installation_auth_token(
|
24
|
+
id: 1,
|
25
|
+
token: "first-token",
|
26
|
+
expires_at: "2018-02-12T10:00:00Z"
|
27
|
+
)
|
28
|
+
|
29
|
+
@cache.store_installation_auth_token(
|
30
|
+
id: 1,
|
31
|
+
token: "second-token",
|
32
|
+
expires_at: "2018-02-12T11:00:00Z"
|
33
|
+
)
|
34
|
+
|
35
|
+
assert_equal "second-token", @cache.lookup_installation_auth_token(id: 1)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_returns_nil_when_looking_up_a_token_that_does_not_exist
|
40
|
+
assert_nil @cache.lookup_installation_auth_token(id: "non-existent")
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_returns_nil_for_tokens_that_have_expired
|
44
|
+
Timecop.freeze(DateTime.parse("2018-02-12T09:00:00+00:00")) do
|
45
|
+
@cache.store_installation_auth_token(
|
46
|
+
id: 1,
|
47
|
+
token: "some-token",
|
48
|
+
expires_at: "2018-02-12T08:00:00Z"
|
49
|
+
)
|
50
|
+
|
51
|
+
assert_nil @cache.lookup_installation_auth_token(id: 1)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_retrieves_a_token_when_the_expiry_is_equal_to_now
|
56
|
+
Timecop.freeze(DateTime.parse("2018-02-12T09:00:00+00:00")) do
|
57
|
+
@cache.store_installation_auth_token(
|
58
|
+
id: 1,
|
59
|
+
token: "some-token",
|
60
|
+
expires_at: "2018-02-12T09:00:00Z"
|
61
|
+
)
|
62
|
+
|
63
|
+
assert_equal "some-token", @cache.lookup_installation_auth_token(id: 1)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#require_relative "../lib/database"
|
2
|
+
|
3
|
+
# http://sequel.jeremyevans.net/rdoc/files/doc/testing_rdoc.html
|
4
|
+
|
5
|
+
class SequelTestCase < Minitest::Test
|
6
|
+
def run(*args, &block)
|
7
|
+
Underway::DB.configure("sqlite:/") # Always assume an in-memory database for now
|
8
|
+
|
9
|
+
Sequel::Model.db.transaction(
|
10
|
+
rollback: :always,
|
11
|
+
auto_savepoint: true
|
12
|
+
){super}
|
13
|
+
end
|
14
|
+
end
|
data/test/test_helper.rb
ADDED
data/underway.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "underway/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "underway"
|
8
|
+
spec.version = Underway::VERSION
|
9
|
+
spec.authors = ["James Martin"]
|
10
|
+
spec.email = ["underway-gem@jmrtn.com"]
|
11
|
+
spec.summary = %q{Quick prototyping helpers for building GitHub Apps.}
|
12
|
+
spec.description = %q{Generate tokens and interact with the GitHub Rest API as a GitHub App.}
|
13
|
+
spec.homepage = "https://github.com/jamesmartin/underway"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
22
|
+
spec.add_development_dependency "minitest"
|
23
|
+
spec.add_development_dependency "pry"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "sinatra"
|
26
|
+
spec.add_development_dependency "timecop"
|
27
|
+
|
28
|
+
spec.add_runtime_dependency "jwt", "~> 2.1"
|
29
|
+
spec.add_runtime_dependency "octokit", "~> 4.0"
|
30
|
+
spec.add_runtime_dependency "sequel"
|
31
|
+
spec.add_runtime_dependency "sqlite3", "~> 1.3"
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: underway
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Martin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-03-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sinatra
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: timecop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: jwt
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.1'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.1'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: octokit
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '4.0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '4.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: sequel
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: sqlite3
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '1.3'
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '1.3'
|
153
|
+
description: Generate tokens and interact with the GitHub Rest API as a GitHub App.
|
154
|
+
email:
|
155
|
+
- underway-gem@jmrtn.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- ".gitignore"
|
161
|
+
- ".ruby-version"
|
162
|
+
- Gemfile
|
163
|
+
- LICENSE
|
164
|
+
- README.md
|
165
|
+
- Rakefile
|
166
|
+
- config.json.example
|
167
|
+
- example/Gemfile
|
168
|
+
- example/app.rb
|
169
|
+
- example/config.json.example
|
170
|
+
- lib/underway.rb
|
171
|
+
- lib/underway/api.rb
|
172
|
+
- lib/underway/database.rb
|
173
|
+
- lib/underway/logger.rb
|
174
|
+
- lib/underway/sawyer_to_json.rb
|
175
|
+
- lib/underway/settings.rb
|
176
|
+
- lib/underway/sinatra.rb
|
177
|
+
- lib/underway/sinatra/app_info.rb
|
178
|
+
- lib/underway/token_cache.rb
|
179
|
+
- lib/underway/version.rb
|
180
|
+
- test/lib/sawyer_to_json_test.rb
|
181
|
+
- test/lib/token_cache_test.rb
|
182
|
+
- test/sequel_helper.rb
|
183
|
+
- test/test_helper.rb
|
184
|
+
- underway.gemspec
|
185
|
+
homepage: https://github.com/jamesmartin/underway
|
186
|
+
licenses:
|
187
|
+
- MIT
|
188
|
+
metadata: {}
|
189
|
+
post_install_message:
|
190
|
+
rdoc_options: []
|
191
|
+
require_paths:
|
192
|
+
- lib
|
193
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
194
|
+
requirements:
|
195
|
+
- - ">="
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
|
+
requirements:
|
200
|
+
- - ">="
|
201
|
+
- !ruby/object:Gem::Version
|
202
|
+
version: '0'
|
203
|
+
requirements: []
|
204
|
+
rubyforge_project:
|
205
|
+
rubygems_version: 2.6.11
|
206
|
+
signing_key:
|
207
|
+
specification_version: 4
|
208
|
+
summary: Quick prototyping helpers for building GitHub Apps.
|
209
|
+
test_files:
|
210
|
+
- test/lib/sawyer_to_json_test.rb
|
211
|
+
- test/lib/token_cache_test.rb
|
212
|
+
- test/sequel_helper.rb
|
213
|
+
- test/test_helper.rb
|