diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 5bb08e5..08a9db1 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -29,6 +29,6 @@ jobs: - name: acceptance env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_DB_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ENV: acceptance run: script/acceptance diff --git a/.gitignore b/.gitignore index f71e75a..a708dd3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,11 +9,6 @@ vendor/gems .local/ .DS_Store .lesshst -*.pem -*.key -*.crt -*.csr -*.secret /.bundle /vendor/gems @@ -35,3 +30,11 @@ coverage/* .idea issue-db-*.gem + +!./spec/fixtures/fake_private_key.pem + +*.pem +*.key +*.crt +*.csr +*.secret diff --git a/Gemfile.lock b/Gemfile.lock index 452975a..bdb4b90 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: issue-db (0.0.2) faraday-retry (~> 2.2, >= 2.2.1) + jwt (~> 2.9, >= 2.9.3) octokit (~> 9.2) redacting-logger (~> 1.4) retryable (~> 3.0, >= 3.0.5) @@ -52,6 +53,8 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.8.2) + jwt (2.9.3) + base64 language_server-protocol (3.17.0.3) logger (1.6.1) minitest (5.25.1) diff --git a/README.md b/README.md index 35ac4b5..be8c148 100644 --- a/README.md +++ b/README.md @@ -186,22 +186,26 @@ This section will go into detail around how you can configure the `issue-db` gem | `ISSUE_DB_CACHE_EXPIRY` | The number of seconds to cache the database in memory. The database is cached in memory to avoid making a request to the GitHub API for every operation. The default value is 60 seconds. | `60` | | `ISSUE_DB_SLEEP` | The number of seconds to sleep between requests to the GitHub API in the event of an error | `3` | | `ISSUE_DB_RETRIES` | The number of retries to make when there is an error making a request to the GitHub API | `10` | -| `GITHUB_TOKEN` | The GitHub personal access token to use for authenticating with the GitHub API. You can also use a GitHub app or pass in your own authenticated Octokit.rb instance | `nil` | +| `ISSUE_DB_GITHUB_TOKEN` | The GitHub personal access token to use for authenticating with the GitHub API. You can also use a GitHub app or pass in your own authenticated Octokit.rb instance | `nil` | ## Authentication 🔒 The `issue-db` gem uses the [`Octokit.rb`](https://github.com/octokit/octokit.rb) library under the hood for interactions with the GitHub API. You have three options for authentication when using the `issue-db` gem: -1. Use a GitHub personal access token by setting the `GITHUB_TOKEN` environment variable -2. Use a GitHub app by setting the `GITHUB_APP_ID`, `GITHUB_APP_INSTALLATION_ID`, and `GITHUB_APP_PRIVATE_KEY` environment variables -3. Pass in your own authenticated `Octokit.rb` instance to the `IssueDB.new` method +> Note: The order displayed below is also the order of priority that this Gem uses to authenticate. + +1. Pass in your own authenticated `Octokit.rb` instance to the `IssueDB.new` method +2. Use a GitHub App by setting the `ISSUE_DB_GITHUB_APP_ID`, `ISSUE_DB_GITHUB_APP_INSTALLATION_ID`, and `ISSUE_DB_GITHUB_APP_KEY` environment variables +3. Use a GitHub personal access token by setting the `ISSUE_DB_GITHUB_TOKEN` environment variable + +> Using a GitHub App is the suggested method Here are examples of each of these options: ### Using a GitHub Personal Access Token ```ruby -# Assuming you have a GitHub personal access token set as the GITHUB_TOKEN env var +# Assuming you have a GitHub personal access token set as the ISSUE_DB_GITHUB_TOKEN env var require "issue_db" db = IssueDB.new("/") # THAT'S IT! 🎉 @@ -209,7 +213,31 @@ db = IssueDB.new("/") # THAT'S IT! 🎉 ### Using a GitHub App +This is the single best way to use the `issue-db` gem because GitHub Apps have increased rate limits, fine-grained permissions, and are more secure than using a personal access token. All you have to do is provide three environment variables and the `issue-db` gem will take care of the rest: + +- `ISSUE_DB_GITHUB_APP_ID` +- `ISSUE_DB_GITHUB_APP_INSTALLATION_ID` +- `ISSUE_DB_GITHUB_APP_KEY` + +Here is an example of how you can use a GitHub app with the `issue-db` gem: + ```ruby +# Assuming you have the following three environment variables set: + +# 1: ISSUE_DB_GITHUB_APP_ID +# app ids are found on the App's settings page + +# 2: ISSUE_DB_GITHUB_APP_INSTALLATION_ID +# installation ids look like this: +# https://github.com/organizations//settings/installations/<8_digit_id> + +# 3. ISSUE_DB_GITHUB_APP_KEY +# app keys are found on the App's settings page and can be downloaded +# format: "-----BEGIN...key\n...END-----\n" (this will be one super long string and that's okay) +# make sure this key in your env is a single line string with newlines as "\n" + +# With all three of these environment variables set, you can proceed with ease! +db = IssueDB.new("/") # THAT'S IT! 🎉 ``` ### Using Your Own Authenticated `Octokit.rb` Instance @@ -230,7 +258,7 @@ db = IssueDB.new("/", octokit_client: octokit) Here is a more advanced example of using the `issue-db` gem that demonstrates many different features of the gem: ```ruby -# Assuming you have a GitHub personal access token set as the GITHUB_TOKEN env var +# Assuming you have a GitHub personal access token set as the ISSUE_DB_GITHUB_TOKEN env var require "issue_db" # The GitHub repository to use as the database diff --git a/issue-db.gemspec b/issue-db.gemspec index 062a70a..f39fde9 100644 --- a/issue-db.gemspec +++ b/issue-db.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |spec| spec.add_dependency "retryable", "~> 3.0", ">= 3.0.5" spec.add_dependency "octokit", "~> 9.2" spec.add_dependency "faraday-retry", "~> 2.2", ">= 2.2.1" + spec.add_dependency "jwt", "~> 2.9", ">= 2.9.3" spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0") diff --git a/lib/issue_db.rb b/lib/issue_db.rb index 18738cc..61b13fb 100644 --- a/lib/issue_db.rb +++ b/lib/issue_db.rb @@ -29,7 +29,7 @@ def initialize(repo, log: nil, octokit_client: nil, label: nil, cache_expiry: ni @log = log || RedactingLogger.new($stdout, level: ENV.fetch("LOG_LEVEL", "INFO").upcase) Retry.setup!(log: @log) @version = VERSION - @client = Authentication.login(octokit_client) + @client = Authentication.login(octokit_client, @log) @repo = Repository.new(repo) @label = label || ENV.fetch("ISSUE_DB_LABEL", "issue-db") @cache_expiry = cache_expiry || ENV.fetch("ISSUE_DB_CACHE_EXPIRY", 60).to_i @@ -41,7 +41,7 @@ def create(key, data, options = {}) end def read(key, options = {}) - db.read(key) + db.read(key, options) end def update(key, data, options = {}) diff --git a/lib/issue_db/authentication.rb b/lib/issue_db/authentication.rb index 355e79a..c07f77f 100644 --- a/lib/issue_db/authentication.rb +++ b/lib/issue_db/authentication.rb @@ -1,20 +1,30 @@ # frozen_string_literal: true require "octokit" +require_relative "utils/github_app" class AuthenticationError < StandardError; end module Authentication - def self.login(client = nil) + def self.login(client = nil, log = nil) # if the client is not nil, use the pre-provided client - return client unless client.nil? + unless client.nil? + log.debug("using pre-provided client") if log + return client + end - # if the client is nil, check for GitHub App env vars - # TODO + # if the client is nil, check for GitHub App env vars first + # first, check if all three of the following env vars are set and have values + # ISSUE_DB_GITHUB_APP_ID, ISSUE_DB_GITHUB_APP_INSTALLATION_ID, ISSUE_DB_GITHUB_APP_KEY + if ENV.fetch("ISSUE_DB_GITHUB_APP_ID", nil) && ENV.fetch("ISSUE_DB_GITHUB_APP_INSTALLATION_ID", nil) && ENV.fetch("ISSUE_DB_GITHUB_APP_KEY", nil) + log.debug("using github app authentication") if log + return GitHubApp.new + end - # if the client is nil and no GitHub App env vars were found, check for the GITHUB_TOKEN - token = ENV.fetch("GITHUB_TOKEN", nil) + # if the client is nil and no GitHub App env vars were found, check for the ISSUE_DB_GITHUB_TOKEN + token = ENV.fetch("ISSUE_DB_GITHUB_TOKEN", nil) if token + log.debug("using github token authentication") if log octokit = Octokit::Client.new(access_token: token, page_size: 100) octokit.auto_paginate = true return octokit diff --git a/lib/issue_db/utils/github_app.rb b/lib/issue_db/utils/github_app.rb new file mode 100644 index 0000000..23088d0 --- /dev/null +++ b/lib/issue_db/utils/github_app.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +# This class provides a wrapper around the Octokit client for GitHub App authentication. +# It handles token generation and refreshing, and delegates method calls to the Octokit client. +# Helpful: https://github.com/octokit/handbook?tab=readme-ov-file#github-app-authentication-json-web-token + +# Why? In some cases, you may not want to have a static long lived token like a GitHub PAT when authenticating... +# with octokit.rb. +# Most importantly, this class will handle automatic token refreshing for you out-of-the-box. Simply provide the... +# correct environment variables, call `GitHubApp.new`, and then use the returned object as you would an Octokit client. + +require "octokit" +require "jwt" + +class GitHubApp + TOKEN_EXPIRATION_TIME = 2700 # 45 minutes + JWT_EXPIRATION_TIME = 600 # 10 minutes + + def initialize + # app ids are found on the App's settings page + @app_id = fetch_env_var("ISSUE_DB_GITHUB_APP_ID").to_i + + # installation ids look like this: + # https://github.com/organizations//settings/installations/<8_digit_id> + @installation_id = fetch_env_var("ISSUE_DB_GITHUB_APP_INSTALLATION_ID").to_i + + # app keys are found on the App's settings page and can be downloaded + # format: "-----BEGIN...key\n...END-----\n" + # make sure this key in your env is a single line string with newlines as "\n" + @app_key = fetch_env_var("ISSUE_DB_GITHUB_APP_KEY").gsub(/\\+n/, "\n") + + @client = nil + @token_refresh_time = nil + end + + private + + # Fetches the value of an environment variable and raises an error if it is not set. + # @param key [String] The name of the environment variable. + # @return [String] The value of the environment variable. + def fetch_env_var(key) + ENV.fetch(key) { raise "environment variable #{key} is not set" } + end + + # Caches the octokit client if it is not nil and the token has not expired + # If it is nil or the token has expired, it creates a new client + # @return [Octokit::Client] The octokit client + def client + if @client.nil? || token_expired? + @client = create_client + end + + @client + end + + # A helper method for generating a JWT token for the GitHub App + # @return [String] The JWT token + def jwt_token + private_key = OpenSSL::PKey::RSA.new(@app_key) + + payload = {}.tap do |opts| + opts[:iat] = Time.now.to_i - 60 # issued at time, 60 seconds in the past to allow for clock drift + opts[:exp] = opts[:iat] + JWT_EXPIRATION_TIME # JWT expiration time (10 minute maximum) + opts[:iss] = @app_id # GitHub App ID + end + + JWT.encode(payload, private_key, "RS256") + end + + # Creates a new octokit client and fetches a new installation access token + # @return [Octokit::Client] The octokit client + def create_client + client = ::Octokit::Client.new(bearer_token: jwt_token) + access_token = client.create_app_installation_access_token(@installation_id)[:token] + client = ::Octokit::Client.new(access_token:) + client.auto_paginate = true + client.per_page = 100 + @token_refresh_time = Time.now + client + end + + # GitHub App installation access tokens expire after 1h + # This method checks if the token has expired and returns true if it has + # It is very cautious and expires tokens at 45 minutes to account for clock drift + # @return [Boolean] True if the token has expired, false otherwise + def token_expired? + @token_refresh_time.nil? || (Time.now - @token_refresh_time) > TOKEN_EXPIRATION_TIME + end + + # This method is called when a method is called on the GitHub class that does not exist. + # It delegates the method call to the Octokit client. + # @param method [Symbol] The name of the method being called. + # @param args [Array] The arguments passed to the method. + # @param block [Proc] An optional block passed to the method. + # @return [Object] The result of the method call on the Octokit client. + def method_missing(method, *args, &block) + client.send(method, *args, &block) + end + + # This method is called to check if the GitHub class responds to a method. + # It checks if the Octokit client responds to the method. + # @param method [Symbol] The name of the method being checked. + # @param include_private [Boolean] Whether to include private methods in the check. + # @return [Boolean] True if the Octokit client responds to the method, false otherwise. + def respond_to_missing?(method, include_private = false) + client.respond_to?(method, include_private) || super + end +end diff --git a/spec/acceptance/acceptance.rb b/spec/acceptance/acceptance.rb index 4929741..57b53f1 100644 --- a/spec/acceptance/acceptance.rb +++ b/spec/acceptance/acceptance.rb @@ -33,7 +33,7 @@ context "#read" do it "successfully reads an issue and returns a record even though it is closed" do - record = db.read("event456") + record = db.read("event456", options) expect(record).to be_a(Record) expect(record.data).to be_a(Hash) expect(record.data["cool"]).to eq(true) diff --git a/spec/fixtures/fake_private_key.pem b/spec/fixtures/fake_private_key.pem new file mode 100644 index 0000000..f6a1652 --- /dev/null +++ b/spec/fixtures/fake_private_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEArCtJrQ+P59A1Pjaf12EfJqltszDpqO0ufsk7N0WRXUeoYZKF +AihwLMIaHbTc1Jn/QEX8WmZGPKIcRlJ9pk2MxQbXRqxM35n61Cb8mYMne+6VXNyl +ZILvRTMXLGIVy/OTmszafTP8Lws9w8vKKLKCax4kEzbfjiRoCgspO7YudFUCxQ6Q +UUxlLPd4/yUaw5A8nUdWZrvPa3CYXG2355yhigRpnOyawoOsvAHUvQruh+z3k6NZ +g6f3eMjsrpeID84TPw1kRs+T+XNsSP21ZUFKYs54bdxlLPphT4iPKNkRwoP0SqJm +97W98B8k7+mtFPZllYGgiHrA4Egnasg9ULiXCwIDAQABAoIBACMoZ9AuWF2nN+gv +cW6jB6B2gs9P0rdLT+5WG4CK9UdOJcVfDUhGh7msHXcpgtrrY6N1ZzXyoq8pD4sQ +t1XpijCF2Bo3fy8+G2mNWJHkpYB6VQf0itW+oyvHZhkLIpZWdDLtWESvA/V7Xy6H +hA3Rfi5vpkBCOV6mcpRyeQYXit74UzDvojXSf8idsjuVDoIXuaoIJMPwyxTWJY+A +8sQ92ecHJ3rQLspxSmXlZYpRtHTNaZlAv1qx605DP/JlV/6bAA3IIWQtrd9Z0Cw/ +rwNtwcP0vB/w6Yq9o8EmJHn7rQuC4NN6lMi/VkwaNCqcPfnRWW7OCtgOhfGAP+kr +vKU5kEECgYEA4hOWtUC4624E4r9NYNOeYcCj9meR5p3FY8vtPLMvpU9r4m3vPldz +ofq/I/Tpo418gWuy02/c4VpHo/8QUBW33yik0YVfL/XWczZwvhabufSTRL3HtIxb +NY5qbgV7yJea1mUuTkiM2obl4x+bR6haHPnHVu+QI480zOzkroF9P2ECgYEAwvUX +dpKebLxzod/UeQLvZFhFXG/qvvAJ7FwxtDRu22znQJR5YVdqd3XDZkfd+64PjDLf +46WMktqu2DclO9eFbKyuLfD0F1OO5z5IHd5dim39/QYo0sJ+y6y6edEM7Of2IxLH +5PkSJLVKAju5t2PJMXDBZBa4HVhNdW+lDHKlCesCgYEAzDvQCVw38g/JACK8P33N +dhe2x9IWv1TGTnqajhx+LYQLPVn9KL+OKcXBSTVmoCcgVDa8LUDANSD+2UuCLCcC +neo0w0cOj+Ax5JFI1qDL+/jT1eTwdc3aVA6dXVk80yEKcyai53upK310TnNuLxUK +m2SWzZXMDCPCGmLj0DYQtOECgYEAs3oIsKsH19ihpxs1MnZGRp2QtSl+9Wpr6EFz +rI88oxqdxfEp0Tg1lmY+jbGJpYI3Y/0N6jfksulJX1ldGLsvZL2P2FFjlPniq/XF +VGH6wU7DLSV3fZd6PSz1uuF+QbbF/MH0blHxpwOSb33mWfMuLCq+jtLvimxZWsx+ +KHh+gSMCgYEA22osS9G3tGpG+x3nZRLXc0lOdKQ51sAo955U1BFscZusP/qQ+b12 +KTHhoUd264C6Nb42XXs9qIkiKWaVblglJPhui61/iWw4s0k4Q9Yf/0KxREmlLbL0 +mm1m2wenfA97PYoheO1esGsatSXNtxPL+C8ywebu38A6hOClQi1BHd4= +-----END RSA PRIVATE KEY----- diff --git a/spec/lib/issue_db/authentication_spec.rb b/spec/lib/issue_db/authentication_spec.rb index f2cf8a2..448ab48 100644 --- a/spec/lib/issue_db/authentication_spec.rb +++ b/spec/lib/issue_db/authentication_spec.rb @@ -19,12 +19,23 @@ expect(Octokit::Client).to receive(:new) .with(access_token: token, page_size: 100) .and_return(client) - expect(ENV).to receive(:fetch).with("GITHUB_TOKEN", nil).and_return(token) + expect(ENV).to receive(:fetch).with("ISSUE_DB_GITHUB_APP_ID", nil).and_return(nil) + expect(ENV).to receive(:fetch).with("ISSUE_DB_GITHUB_TOKEN", nil).and_return(token) + expect(Authentication.login).to eq(client) + end + + it "returns a hydrated octokit client from a GitHub App" do + expect(ENV).to receive(:fetch).with("ISSUE_DB_GITHUB_APP_ID", nil).and_return("123") + expect(ENV).to receive(:fetch).with("ISSUE_DB_GITHUB_APP_INSTALLATION_ID", nil).and_return("456") + expect(ENV).to receive(:fetch).with("ISSUE_DB_GITHUB_APP_KEY", nil).and_return("-----KEY-----") + + expect(GitHubApp).to receive(:new).and_return(client) expect(Authentication.login).to eq(client) end it "raises an authentication error when no auth methods pass for octokit" do - expect(ENV).to receive(:fetch).with("GITHUB_TOKEN", nil).and_return(nil) + expect(ENV).to receive(:fetch).with("ISSUE_DB_GITHUB_APP_ID", nil).and_return(nil) + expect(ENV).to receive(:fetch).with("ISSUE_DB_GITHUB_TOKEN", nil).and_return(nil) expect do Authentication.login end.to raise_error( diff --git a/spec/lib/issue_db/models/record_spec.rb b/spec/lib/issue_db/models/record_spec.rb index d162e9d..8df811f 100644 --- a/spec/lib/issue_db/models/record_spec.rb +++ b/spec/lib/issue_db/models/record_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" require_relative "../../../../lib/issue_db/models/record" -RSpec.describe Record do +describe Record do let(:valid_issue) do double( "issue", diff --git a/spec/lib/issue_db/utils/generate_spec.rb b/spec/lib/issue_db/utils/generate_spec.rb index 78b1236..85acff3 100644 --- a/spec/lib/issue_db/utils/generate_spec.rb +++ b/spec/lib/issue_db/utils/generate_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" require_relative "../../../../lib/issue_db/utils/generate" -RSpec.describe Generate do +describe Generate do include Generate let(:data) do diff --git a/spec/lib/issue_db/utils/github_app_spec.rb b/spec/lib/issue_db/utils/github_app_spec.rb new file mode 100644 index 0000000..8a08e44 --- /dev/null +++ b/spec/lib/issue_db/utils/github_app_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require "spec_helper" +require_relative "../../../../lib/issue_db/utils/github_app" + +describe GitHubApp, :vcr do + let(:app_id) { "123" } + let(:installation_id) { "456" } + let(:app_key) { File.read("spec/fixtures/fake_private_key.pem") } + let(:jwt_token) { "jwt_token" } + let(:access_token) { "access_token" } + let(:client) { instance_double(Octokit::Client) } + + before do + allow(ENV).to receive(:fetch).with("ISSUE_DB_GITHUB_APP_ID").and_return(app_id) + allow(ENV).to receive(:fetch).with("ISSUE_DB_GITHUB_APP_INSTALLATION_ID").and_return(installation_id) + allow(ENV).to receive(:fetch).with("ISSUE_DB_GITHUB_APP_KEY").and_return(app_key) + allow(ENV).to receive(:fetch).with("http_proxy", nil).and_return(nil) + + allow(client).to receive(:auto_paginate=).with(true).and_return(true) + allow(client).to receive(:per_page=).with(100).and_return(100) + end + + describe "#initialize" do + it "initializes with environment variables" do + github_app = GitHubApp.new + expect(github_app.instance_variable_get(:@app_id)).to eq(app_id.to_i) + expect(github_app.instance_variable_get(:@installation_id)).to eq(installation_id.to_i) + expect(github_app.instance_variable_get(:@app_key)).to eq(app_key.gsub(/\\+n/, "\n")) + end + end + + describe "#client" do + let(:github_app) { GitHubApp.new } + + before do + allow(github_app).to receive(:jwt_token).and_return(jwt_token) + allow(client).to receive(:create_app_installation_access_token).with(installation_id.to_i).and_return(token: access_token) + allow(Octokit::Client).to receive(:new).with(bearer_token: jwt_token).and_return(client) + allow(Octokit::Client).to receive(:new).with(access_token: access_token).and_return(client) + end + + context "when client is nil" do + it "creates a new client" do + expect(github_app.send(:client)).to eq(client) + end + end + + context "when token is expired" do + it "creates a new client" do + github_app.instance_variable_set(:@token_refresh_time, Time.now - GitHubApp::TOKEN_EXPIRATION_TIME - 1) + expect(github_app.send(:client)).to eq(client) + end + end + + context "when token is not expired" do + it "returns the cached client" do + github_app.instance_variable_set(:@client, client) + github_app.instance_variable_set(:@token_refresh_time, Time.now) + expect(github_app.send(:client)).to eq(client) + end + end + end + + describe "#jwt_token" do + it "generates a JWT token" do + github_app = GitHubApp.new + private_key = OpenSSL::PKey::RSA.new(app_key.gsub(/\\+n/, "\n")) + payload = { + iat: Time.now.to_i - 60, + exp: Time.now.to_i - 60 + GitHubApp::JWT_EXPIRATION_TIME, + iss: app_id.to_i + } + token = JWT.encode(payload, private_key, "RS256") + expect(github_app.send(:jwt_token)).to eq(token) + end + end + + describe "#method_missing" do + it "delegates method calls to the Octokit client" do + github_app = GitHubApp.new + allow(github_app).to receive(:client).and_return(client) + expect(client).to receive(:rate_limit) + github_app.rate_limit + end + end + + describe "#respond_to_missing?" do + it "checks if the Octokit client responds to a method" do + github_app = GitHubApp.new + allow(github_app).to receive(:client).and_return(client) + allow(client).to receive(:respond_to?).with(:rate_limit, false).and_return(true) + expect(github_app.respond_to?(:rate_limit)).to be true + end + end + + describe "auth with VCR" do + it "fails because no env vars are provided at all" do + github_app = GitHubApp.new + expect { github_app.rate_limit }.to raise_error(StandardError, /401 - A JSON web token could not be decoded/) + end + + it "successfully authenticates with the GitHub App" do + github_app = GitHubApp.new + expect(github_app.rate_limit.remaining).to eq(5000) + end + end +end diff --git a/spec/lib/issue_db/utils/init_spec.rb b/spec/lib/issue_db/utils/init_spec.rb index 97d69e2..e2ab5d9 100644 --- a/spec/lib/issue_db/utils/init_spec.rb +++ b/spec/lib/issue_db/utils/init_spec.rb @@ -16,7 +16,7 @@ def initialize(client, repo, label, log) end end -RSpec.describe Init do +describe Init do let(:client) { double("client") } let(:repo) { double("repo", full_name: "user/repo") } let(:label) { "issue-db" } diff --git a/spec/lib/issue_db_spec.rb b/spec/lib/issue_db_spec.rb index 5dfbb96..4278f78 100644 --- a/spec/lib/issue_db_spec.rb +++ b/spec/lib/issue_db_spec.rb @@ -25,7 +25,7 @@ context "#read" do it "makes a read operation" do - expect(database).to receive(:read).with("event123").and_return(record) + expect(database).to receive(:read).with("event123", {}).and_return(record) record = subject.read("event123") expect(record.data["cool"]).to eq(true) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bc8b596..abd5086 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -47,6 +47,12 @@ config.cassette_library_dir = "spec/vcr_cassettes" config.hook_into :webmock config.configure_rspec_metadata! + config.filter_sensitive_data("") { ENV["ISSUE_DB_GITHUB_TOKEN"] } config.filter_sensitive_data("") { ENV["GITHUB_TOKEN"] } + config.filter_sensitive_data("") do |interaction| + if interaction.request.headers["Authorization"] + interaction.request.headers["Authorization"].first + end + end # config.default_cassette_options = { record: :new_episodes } end diff --git a/spec/vcr_cassettes/GitHubApp/auth_with_VCR/fails_because_no_env_vars_are_provided_at_all.yml b/spec/vcr_cassettes/GitHubApp/auth_with_VCR/fails_because_no_env_vars_are_provided_at_all.yml new file mode 100644 index 0000000..3a3b9a8 --- /dev/null +++ b/spec/vcr_cassettes/GitHubApp/auth_with_VCR/fails_because_no_env_vars_are_provided_at_all.yml @@ -0,0 +1,62 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.github.com/app/installations/456/access_tokens + body: + encoding: UTF-8 + string: "{}" + headers: + Accept: + - application/vnd.github.v3+json + User-Agent: + - Octokit Ruby Gem 9.2.0 + Content-Type: + - application/json + Authorization: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 401 + message: Unauthorized + headers: + Date: + - Fri, 29 Nov 2024 04:21:53 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '117' + X-Github-Media-Type: + - github.v3; format=json + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Access-Control-Allow-Origin: + - "*" + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + X-Frame-Options: + - deny + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - '0' + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Content-Security-Policy: + - default-src 'none' + Vary: + - Accept-Encoding, Accept, X-Requested-With + Server: + - github.com + X-Github-Request-Id: + - C6DC:3D922A:3B43956:3C56B5A:67494161 + body: + encoding: UTF-8 + string: '{"message":"A JSON web token could not be decoded","documentation_url":"https://docs.github.com/rest","status":"401"}' + recorded_at: Fri, 29 Nov 2024 04:21:53 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr_cassettes/GitHubApp/auth_with_VCR/successfully_authenticates_with_the_GitHub_App.yml b/spec/vcr_cassettes/GitHubApp/auth_with_VCR/successfully_authenticates_with_the_GitHub_App.yml new file mode 100644 index 0000000..223a743 --- /dev/null +++ b/spec/vcr_cassettes/GitHubApp/auth_with_VCR/successfully_authenticates_with_the_GitHub_App.yml @@ -0,0 +1,143 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.github.com/app/installations/456/access_tokens + body: + encoding: UTF-8 + string: "{}" + headers: + Accept: + - application/vnd.github.v3+json + User-Agent: + - Octokit Ruby Gem 9.2.0 + Content-Type: + - application/json + Authorization: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 201 + message: Created + headers: + Date: + - Fri, 29 Nov 2024 04:35:39 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '173' + Cache-Control: + - public, max-age=60, s-maxage=60 + Vary: + - Accept,Accept-Encoding, Accept, X-Requested-With + Etag: + - '"373477d3ccb7010072dc51b99fca284042293cfb4e679e6f5367de2ebabcb9ed"' + X-Github-Media-Type: + - github.v3; format=json + X-Github-Api-Version-Selected: + - '2022-11-28' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Access-Control-Allow-Origin: + - "*" + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + X-Frame-Options: + - deny + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - '0' + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Content-Security-Policy: + - default-src 'none' + Server: + - github.com + X-Github-Request-Id: + - C8C1:6E61B:3E45927:3F5C193:6749449B + body: + encoding: UTF-8 + string: '{"token":"ghs_","expires_at":"2024-11-29T05:35:39Z","permissions":{"issues":"write","metadata":"read"},"repository_selection":"selected"}' + recorded_at: Fri, 29 Nov 2024 04:35:39 GMT +- request: + method: get + uri: https://api.github.com/rate_limit + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - application/vnd.github.v3+json + User-Agent: + - Octokit Ruby Gem 9.2.0 + Content-Type: + - application/json + Authorization: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 29 Nov 2024 04:35:40 GMT + Content-Type: + - application/json; charset=utf-8 + Cache-Control: + - no-cache + X-Github-Media-Type: + - github.v3; format=json + X-Accepted-Github-Permissions: + - allows_permissionless_access=true + X-Github-Api-Version-Selected: + - '2022-11-28' + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '5000' + X-Ratelimit-Reset: + - '1732858540' + X-Ratelimit-Used: + - '0' + X-Ratelimit-Resource: + - core + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Access-Control-Allow-Origin: + - "*" + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + X-Frame-Options: + - deny + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - '0' + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Content-Security-Policy: + - default-src 'none' + Vary: + - Accept-Encoding, Accept, X-Requested-With + Transfer-Encoding: + - chunked + Server: + - github.com + X-Github-Request-Id: + - C8C2:1730D2:13061DDA:134B50AB:6749449B + body: + encoding: ASCII-8BIT + string: '{"resources":{"core":{"limit":5000,"used":0,"remaining":5000,"reset":1732858540},"search":{"limit":30,"used":0,"remaining":30,"reset":1732855000},"graphql":{"limit":5000,"used":0,"remaining":5000,"reset":1732858540},"integration_manifest":{"limit":5000,"used":0,"remaining":5000,"reset":1732858540},"source_import":{"limit":100,"used":0,"remaining":100,"reset":1732855000},"code_scanning_upload":{"limit":1000,"used":0,"remaining":1000,"reset":1732858540},"actions_runner_registration":{"limit":10000,"used":0,"remaining":10000,"reset":1732858540},"scim":{"limit":15000,"used":0,"remaining":15000,"reset":1732858540},"dependency_snapshots":{"limit":100,"used":0,"remaining":100,"reset":1732855000},"audit_log":{"limit":1750,"used":0,"remaining":1750,"reset":1732858540},"audit_log_streaming":{"limit":15,"used":0,"remaining":15,"reset":1732858540},"code_search":{"limit":10,"used":0,"remaining":10,"reset":1732855000}},"rate":{"limit":5000,"used":0,"remaining":5000,"reset":1732858540}}' + recorded_at: Fri, 29 Nov 2024 04:35:40 GMT +recorded_with: VCR 6.3.1 diff --git a/vendor/cache/jwt-2.9.3.gem b/vendor/cache/jwt-2.9.3.gem new file mode 100644 index 0000000..d44fee5 Binary files /dev/null and b/vendor/cache/jwt-2.9.3.gem differ