minty 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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +4 -0
  3. data/.devcontainer/Dockerfile +19 -0
  4. data/.devcontainer/devcontainer.json +37 -0
  5. data/.env.example +2 -0
  6. data/.gemrelease +2 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +33 -0
  8. data/.github/dependabot.yml +10 -0
  9. data/.github/stale.yml +20 -0
  10. data/.gitignore +18 -0
  11. data/.rspec +3 -0
  12. data/.rubocop.yml +9 -0
  13. data/CODE_OF_CONDUCT.md +3 -0
  14. data/DEPLOYMENT.md +61 -0
  15. data/DEVELOPMENT.md +35 -0
  16. data/Dockerfile +5 -0
  17. data/EXAMPLES.md +195 -0
  18. data/Gemfile +21 -0
  19. data/Gemfile.lock +250 -0
  20. data/Guardfile +39 -0
  21. data/LICENSE +21 -0
  22. data/Makefile +5 -0
  23. data/README.md +88 -0
  24. data/RUBYGEM.md +9 -0
  25. data/Rakefile +33 -0
  26. data/codecov.yml +22 -0
  27. data/lib/minty/algorithm.rb +7 -0
  28. data/lib/minty/api/authentication_endpoints.rb +30 -0
  29. data/lib/minty/api/v2.rb +8 -0
  30. data/lib/minty/client.rb +7 -0
  31. data/lib/minty/exception.rb +50 -0
  32. data/lib/minty/mixins/api_token_struct.rb +4 -0
  33. data/lib/minty/mixins/headers.rb +19 -0
  34. data/lib/minty/mixins/httpproxy.rb +125 -0
  35. data/lib/minty/mixins/initializer.rb +38 -0
  36. data/lib/minty/mixins/validation.rb +113 -0
  37. data/lib/minty/mixins.rb +23 -0
  38. data/lib/minty/version.rb +5 -0
  39. data/lib/minty.rb +11 -0
  40. data/lib/minty_client.rb +4 -0
  41. data/minty.gemspec +39 -0
  42. data/publish_rubygem.sh +10 -0
  43. data/spec/integration/lib/minty/api/api_authentication_spec.rb +122 -0
  44. data/spec/integration/lib/minty/minty_client_spec.rb +92 -0
  45. data/spec/lib/minty/client_spec.rb +223 -0
  46. data/spec/lib/minty/mixins/httpproxy_spec.rb +658 -0
  47. data/spec/lib/minty/mixins/initializer_spec.rb +121 -0
  48. data/spec/lib/minty/mixins/token_management_spec.rb +129 -0
  49. data/spec/lib/minty/mixins/validation_spec.rb +559 -0
  50. data/spec/spec_helper.rb +75 -0
  51. data/spec/support/credentials.rb +14 -0
  52. data/spec/support/dummy_class.rb +20 -0
  53. data/spec/support/dummy_class_for_proxy.rb +6 -0
  54. data/spec/support/dummy_class_for_restclient.rb +4 -0
  55. data/spec/support/dummy_class_for_tokens.rb +18 -0
  56. data/spec/support/import_users.json +13 -0
  57. data/spec/support/stub_response.rb +3 -0
  58. metadata +366 -0
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'addressable/uri'
4
+ require 'retryable'
5
+ require_relative '../exception'
6
+
7
+ module Minty
8
+ module Mixins
9
+ module HTTPProxy
10
+ attr_accessor :headers, :base_uri, :timeout, :retry_count
11
+
12
+ DEFAULT_RETRIES = 3
13
+ MAX_ALLOWED_RETRIES = 10
14
+ MAX_REQUEST_RETRY_JITTER = 250
15
+ MAX_REQUEST_RETRY_DELAY = 1000
16
+ MIN_REQUEST_RETRY_DELAY = 250
17
+ BASE_DELAY = 100
18
+
19
+ %i[get post post_file put patch delete delete_with_body].each do |method|
20
+ define_method(method) do |uri, body = {}, extra_headers = {}|
21
+ body = body.delete_if { |_, v| v.nil? }
22
+ token = get_token
23
+ authorization_header(token) unless token.nil?
24
+ request_with_retry(method, uri, body, extra_headers)
25
+ end
26
+ end
27
+
28
+ def retry_options
29
+ sleep_timer = lambda do |attempt|
30
+ wait = BASE_DELAY * (2**attempt - 1)
31
+ wait += rand(wait + 1..wait + MAX_REQUEST_RETRY_JITTER)
32
+ wait = [MAX_REQUEST_RETRY_DELAY, wait].min
33
+ wait = [MIN_REQUEST_RETRY_DELAY, wait].max
34
+ wait / 1000.to_f.round(2)
35
+ end
36
+
37
+ tries = 1 + [Integer(retry_count || DEFAULT_RETRIES), MAX_ALLOWED_RETRIES].min
38
+
39
+ {
40
+ tries: tries,
41
+ sleep: sleep_timer,
42
+ on: Minty::RateLimitEncountered
43
+ }
44
+ end
45
+
46
+ def encode_uri(uri)
47
+ path = base_uri ? Addressable::URI.new(path: uri).normalized_path : Addressable::URI.escape(uri)
48
+ url(path)
49
+ end
50
+
51
+ def url(path)
52
+ "#{base_uri}#{path}"
53
+ end
54
+
55
+ def add_headers(h = {})
56
+ raise ArgumentError, 'Headers must be an object which responds to #to_hash' unless h.respond_to?(:to_hash)
57
+
58
+ @headers ||= {}
59
+ @headers.merge!(h.to_hash)
60
+ end
61
+
62
+ def safe_parse_json(body)
63
+ JSON.parse(body.to_s)
64
+ rescue JSON::ParserError
65
+ body
66
+ end
67
+
68
+ def request_with_retry(method, uri, body = {}, extra_headers = {})
69
+ Retryable.retryable(retry_options) do
70
+ request(method, uri, body, extra_headers)
71
+ end
72
+ end
73
+
74
+ def request(method, uri, body = {}, extra_headers = {})
75
+ result = case method
76
+ when :get
77
+ @headers ||= {}
78
+ get_headers = @headers.merge({ params: body }).merge(extra_headers)
79
+ call(:get, encode_uri(uri), timeout, get_headers)
80
+ when :delete
81
+ @headers ||= {}
82
+ delete_headers = @headers.merge({ params: body })
83
+ call(:delete, encode_uri(uri), timeout, delete_headers)
84
+ when :delete_with_body
85
+ call(:delete, encode_uri(uri), timeout, headers, body.to_json)
86
+ when :post_file
87
+ body.merge!(multipart: true)
88
+ post_file_headers = headers.slice(*headers.keys - ['Content-Type'])
89
+ call(:post, encode_uri(uri), timeout, post_file_headers, body)
90
+ else
91
+ call(method, encode_uri(uri), timeout, headers, body.to_json)
92
+ end
93
+
94
+ case result.code
95
+ when 200...226 then safe_parse_json(result.body)
96
+ when 400 then raise Minty::BadRequest.new(result.body, code: result.code, headers: result.headers)
97
+ when 401 then raise Minty::Unauthorized.new(result.body, code: result.code, headers: result.headers)
98
+ when 403 then raise Minty::AccessDenied.new(result.body, code: result.code, headers: result.headers)
99
+ when 404 then raise Minty::NotFound.new(result.body, code: result.code, headers: result.headers)
100
+ when 429 then raise Minty::RateLimitEncountered.new(result.body, code: result.code,
101
+ headers: result.headers)
102
+ when 500 then raise Minty::ServerError.new(result.body, code: result.code, headers: result.headers)
103
+ else raise Minty::Unsupported.new(result.body, code: result.code, headers: result.headers)
104
+ end
105
+ end
106
+
107
+ def call(method, url, timeout, headers, body = nil)
108
+ RestClient::Request.execute(
109
+ method: method,
110
+ url: url,
111
+ timeout: timeout,
112
+ headers: headers,
113
+ payload: body
114
+ )
115
+ rescue RestClient::Exception => e
116
+ case e
117
+ when RestClient::RequestTimeout
118
+ raise Minty::RequestTimeout, e.message
119
+ else
120
+ e.response
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Minty
6
+ module Mixins
7
+ module Initializer
8
+ def initialize(config)
9
+ options = Hash[config.map { |(k, v)| [k.to_sym, v] }]
10
+
11
+ @base_uri = base_url(options)
12
+ @timeout = options[:timeout] || 10
13
+ @retry_count = options[:retry_count]
14
+
15
+ extend Minty::Api::AuthenticationEndpoints
16
+
17
+ @client_id = options[:client_id]
18
+ @client_secret = options[:client_secret]
19
+ @application_id = options[:application_id]
20
+ @organization = options[:organization]
21
+ @headers = client_headers(@client_id, @client_secret, @application_id)
22
+ end
23
+
24
+ def self.included(klass)
25
+ klass.send :prepend, Initializer
26
+ end
27
+
28
+ private
29
+
30
+ def base_url(options)
31
+ @domain = options[:domain] || options[:namespace]
32
+ raise InvalidApiNamespace, 'API namespace must supply an API domain' if @domain.to_s.empty?
33
+
34
+ "https://#{@domain}"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zache'
4
+
5
+ class Zache
6
+ def last(key)
7
+ @hash[key][:value] if @hash.key?(key)
8
+ end
9
+ end
10
+
11
+ module Minty
12
+ module Mixins
13
+ module Validation
14
+ class JWTAlgorithm
15
+ private_class_method :new
16
+
17
+ def name
18
+ raise 'Must be overriden by the subclasses'
19
+ end
20
+ end
21
+
22
+ module Algorithm
23
+ class HS256 < JWTAlgorithm
24
+ class << self
25
+ private :new
26
+
27
+ def secret(secret)
28
+ new secret
29
+ end
30
+ end
31
+
32
+ attr_accessor :secret
33
+
34
+ def initialize(secret)
35
+ raise Minty::InvalidParameter, 'Must supply a valid secret' if secret.to_s.empty?
36
+
37
+ @secret = secret
38
+ end
39
+
40
+ def name
41
+ 'HS256'
42
+ end
43
+ end
44
+
45
+ class RS256 < JWTAlgorithm
46
+ include Minty::Mixins::HTTPProxy
47
+
48
+ @@cache = Zache.new.freeze
49
+
50
+ class << self
51
+ private :new
52
+
53
+ def jwks_url(url, lifetime: 10 * 60)
54
+ new url, lifetime
55
+ end
56
+
57
+ def remove_jwks
58
+ @@cache.remove_by { true }
59
+ end
60
+ end
61
+
62
+ def initialize(jwks_url, lifetime)
63
+ raise Minty::InvalidParameter, 'Must supply a valid jwks_url' if jwks_url.to_s.empty?
64
+
65
+ unless lifetime.is_a?(Integer) && lifetime >= 0
66
+ raise Minty::InvalidParameter,
67
+ 'Must supply a valid lifetime'
68
+ end
69
+
70
+ @lifetime = lifetime
71
+ @jwks_url = jwks_url
72
+ @did_fetch_jwks = false
73
+ end
74
+
75
+ def name
76
+ 'RS256'
77
+ end
78
+
79
+ def jwks(force: false)
80
+ result = fetch_jwks if force
81
+
82
+ if result
83
+ @@cache.put(@jwks_url, result, lifetime: @lifetime)
84
+ return result
85
+ end
86
+
87
+ previous_value = @@cache.last(@jwks_url)
88
+
89
+ @@cache.get(@jwks_url, lifetime: @lifetime, dirty: true) do
90
+ new_value = fetch_jwks
91
+
92
+ raise Minty::InvalidIdToken, 'Could not fetch the JWK set' unless new_value || previous_value
93
+
94
+ new_value || previous_value
95
+ end
96
+ end
97
+
98
+ def fetched_jwks?
99
+ @did_fetch_jwks
100
+ end
101
+
102
+ private
103
+
104
+ def fetch_jwks
105
+ result = request_with_retry(:get, @jwks_url, {}, {})
106
+ @did_fetch_jwks = result.is_a?(Hash) && result.key?('keys')
107
+ result if @did_fetch_jwks
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'rest-client'
5
+ require 'uri'
6
+
7
+ require 'minty/mixins/api_token_struct'
8
+ require 'minty/mixins/headers'
9
+ require 'minty/mixins/httpproxy'
10
+ require 'minty/mixins/initializer'
11
+ require 'minty/mixins/validation'
12
+
13
+ require 'minty/api/authentication_endpoints'
14
+ require 'minty/api/v2'
15
+
16
+ module Minty
17
+ # Collecting dependencies here
18
+ module Mixins
19
+ include Minty::Mixins::Headers
20
+ include Minty::Mixins::HTTPProxy
21
+ include Minty::Mixins::Initializer
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minty
4
+ VERSION = '1.0.0'
5
+ end
data/lib/minty.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minty/version'
4
+ require 'minty/mixins'
5
+ require 'minty/exception'
6
+ require 'minty/algorithm'
7
+ require 'minty/client'
8
+ require 'minty_client'
9
+
10
+ module Minty
11
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MintyClient < Minty::Client
4
+ end
data/minty.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
3
+ require 'minty/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'minty'
7
+ s.version = Minty::VERSION
8
+ s.authors = ['Minty']
9
+ s.email = ['support@minty.page']
10
+ s.homepage = 'https://github.com/mintypage/ruby'
11
+ s.summary = 'Minty API Client'
12
+ s.description = 'Ruby toolkit for Minty API https://minty.page'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
17
+ s.require_paths = ['lib']
18
+
19
+ s.add_runtime_dependency 'rest-client', '~> 2.1'
20
+ s.add_runtime_dependency 'jwt', '~> 2.5'
21
+ s.add_runtime_dependency 'zache', '~> 0.12'
22
+ s.add_runtime_dependency 'addressable', '~> 2.8'
23
+ s.add_runtime_dependency 'retryable', '~> 3.0'
24
+
25
+ s.add_development_dependency 'bundler'
26
+ s.add_development_dependency 'rake', '~> 13.0'
27
+ s.add_development_dependency 'fuubar', '~> 2.0'
28
+ s.add_development_dependency 'guard-rspec', '~> 4.5' unless ENV['CIRCLECI']
29
+ s.add_development_dependency 'dotenv-rails', '~> 2.0'
30
+ s.add_development_dependency 'pry', '~> 0.10'
31
+ s.add_development_dependency 'pry-nav', '~> 0.2'
32
+ s.add_development_dependency 'rspec', '~> 3.11'
33
+ s.add_development_dependency 'rack-test', '~> 0.6'
34
+ s.add_development_dependency 'rack', '~> 2.1'
35
+ s.add_development_dependency 'simplecov', '~> 0.9'
36
+ s.add_development_dependency 'faker', '~> 2.0'
37
+ s.add_development_dependency 'gem-release', '~> 0.7'
38
+ s.license = 'MIT'
39
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Create directory for rubygems credentials
4
+ mkdir /root/.gem
5
+ # Get API key from rubygems.org
6
+ curl -u "$RUBYGEMS_EMAIL":"$RUBYGEMS_PASSWORD" https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials; chmod 0600 ~/.gem/credentials
7
+ # Build Gem
8
+ gem build minty.gemspec
9
+ # Publish Gem
10
+ gem push minty-*.gem
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ describe Minty::Api::AuthenticationEndpoints do
5
+ attr_reader :client, :test_user_email, :test_user_pwd, :test_user
6
+
7
+ before(:all) do
8
+ @client = MintyClient.new(v2_creds)
9
+
10
+ @test_user_email = "#{entity_suffix}-username-1@minty.page"
11
+ @test_user_pwd = '23kejn2jk3en2jke2jk3be2jk3ber'
12
+
13
+ VCR.use_cassette('Minty_Api_AuthenticationEndpoints/create_test_user') do
14
+ @test_user ||= @client.signup(
15
+ test_user_email,
16
+ test_user_pwd
17
+ )
18
+ end
19
+ end
20
+
21
+ after(:all) do
22
+ VCR.use_cassette('Minty_Api_AuthenticationEndpoints/delete_test_user') do
23
+ @client.delete_user("minty|#{test_user['_id']}")
24
+ end
25
+ end
26
+
27
+ describe '.signup', vcr: true do
28
+ it 'should signup a new user' do
29
+ expect(test_user).to(include('_id', 'email'))
30
+ end
31
+
32
+ it 'should return the correct email address' do
33
+ expect(test_user['email']).to eq test_user_email
34
+ end
35
+ end
36
+
37
+ describe '.change_password', vcr: true do
38
+ it 'should trigger a password reset' do
39
+ expect(
40
+ @client.change_password(test_user_email, '')
41
+ ).to(include("We've just sent you an email to reset your password."))
42
+ end
43
+ end
44
+
45
+ describe '.saml_metadata', vcr: true do
46
+ it 'should retrieve SAML metadata' do
47
+ expect(@client.saml_metadata).to(include('<EntityDescriptor'))
48
+ end
49
+ end
50
+
51
+ describe '.wsfed_metadata', vcr: true do
52
+ it 'should retrieve WSFED metadata' do
53
+ expect(@client.wsfed_metadata).to(include('<EntityDescriptor'))
54
+ end
55
+ end
56
+
57
+ describe '.userinfo', vcr: true do
58
+ it 'should fail as not authorized' do
59
+ expect do
60
+ @client.userinfo('invalid_token')
61
+ end.to raise_error Minty::Unauthorized
62
+ end
63
+
64
+ it 'should return the userinfo' do
65
+ tokens = @client.login_with_resource_owner(test_user_email, test_user_pwd)
66
+ expect(@client.userinfo(tokens['access_token'])).to(
67
+ include('email' => test_user_email)
68
+ )
69
+ end
70
+ end
71
+
72
+ describe '.login_with_resource_owner', vcr: true do
73
+ it 'should fail with an incorrect email' do
74
+ expect do
75
+ @client.login_with_resource_owner(
76
+ "#{test_user['email']}_invalid",
77
+ test_user_pwd
78
+ )
79
+ end.to raise_error Minty::AccessDenied
80
+ end
81
+
82
+ it 'should fail with an incorrect password' do
83
+ expect do
84
+ @client.login_with_resource_owner(
85
+ test_user['email'],
86
+ "#{test_user_pwd}_invalid"
87
+ )
88
+ end.to raise_error Minty::AccessDenied
89
+ end
90
+
91
+ it 'should login successfully with a default scope' do
92
+ expect(
93
+ @client.login_with_resource_owner(
94
+ test_user['email'],
95
+ test_user_pwd
96
+ ).token
97
+ ).to_not be_empty
98
+ end
99
+
100
+ it 'should fail with an invalid audience' do
101
+ expect do
102
+ @client.login_with_resource_owner(
103
+ test_user['email'],
104
+ test_user_pwd,
105
+ scope: 'test:scope',
106
+ audience: 'https://brucke.club/invalid/api/v1/'
107
+ )
108
+ end.to raise_error Minty::BadRequest
109
+ end
110
+
111
+ it 'should login successfully with a custom audience' do
112
+ expect(
113
+ @client.login_with_resource_owner(
114
+ test_user['email'],
115
+ test_user_pwd,
116
+ scope: 'test:scope',
117
+ audience: 'https://brucke.club/custom/api/v1/'
118
+ ).token
119
+ ).to_not be_empty
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ describe Minty::Client do
5
+ shared_examples 'invalid credentials' do |credentials, error|
6
+ it "raise an error with credentials #{credentials}" do
7
+ if error.nil?
8
+ expect { MintyClient.new(credentials) }.to raise_error
9
+ else
10
+ expect { MintyClient.new(credentials) }.to raise_error(error)
11
+ end
12
+ end
13
+ end
14
+
15
+ it_should_behave_like 'invalid credentials', {
16
+ namespace: 'samples.minty.page'
17
+ }, Minty::InvalidCredentials
18
+
19
+ it_should_behave_like 'invalid credentials', {
20
+ namespace: 'samples.minty.page', client_id: 'client_id'
21
+ }, Minty::InvalidCredentials
22
+
23
+ it_should_behave_like 'invalid credentials', {
24
+ namespace: 'samples.minty.page', client_secret: 'secret'
25
+ }, Minty::InvalidCredentials
26
+
27
+ it_should_behave_like 'invalid credentials', {
28
+ namespace: 'samples.minty.page', api_version: 2
29
+ }, Minty::InvalidCredentials
30
+
31
+ it_should_behave_like 'invalid credentials', {}, Minty::InvalidApiNamespace
32
+
33
+ it_should_behave_like 'invalid credentials', {
34
+ api_version: 2
35
+ }, Minty::InvalidApiNamespace
36
+
37
+ it_should_behave_like 'invalid credentials', {
38
+ api_version: 1
39
+ }, Minty::InvalidApiNamespace
40
+
41
+ it_should_behave_like 'invalid credentials', {
42
+ client_id: 'client_id', client_secret: 'secret'
43
+ }, Minty::InvalidApiNamespace
44
+
45
+ it_should_behave_like 'invalid credentials', {
46
+ api_version: 2, token: 'token'
47
+ }, Minty::InvalidApiNamespace
48
+
49
+ let(:v2_credentials) { { domain: 'test.minty.page' } }
50
+
51
+ shared_examples 'valid credentials' do
52
+ it { expect { MintyClient.new(credentials) }.to_not raise_error }
53
+ end
54
+
55
+ it_should_behave_like 'valid credentials' do
56
+ let(:credentials) { v2_credentials.merge(token: 'TEST_API_TOKEN') }
57
+ end
58
+
59
+ it_should_behave_like 'valid credentials' do
60
+ let(:credentials) { v2_credentials.merge(access_token: 'TEST_API_TOKEN') }
61
+ end
62
+
63
+ context 'client headers' do
64
+ let(:client) { Minty::Client.new(v2_credentials.merge(access_token: 'abc123', domain: 'myhost.minty.page')) }
65
+ let(:headers) { client.headers }
66
+ let(:telemetry) { JSON.parse(Base64.urlsafe_decode64(headers['Minty-Client'])) }
67
+
68
+ it 'has the correct headers present' do
69
+ expect(headers.keys.sort).to eql(%w[Minty-Client Authorization Content-Type])
70
+ end
71
+
72
+ it 'uses the correct access token' do
73
+ expect(headers['Authorization']).to eql 'Bearer abc123'
74
+ end
75
+
76
+ it 'is always json' do
77
+ expect(headers['Content-Type']).to eql 'application/json'
78
+ end
79
+
80
+ it 'should include the correct name in telemetry data' do
81
+ expect(telemetry['name']).to eq('ruby')
82
+ end
83
+
84
+ it 'should include the correct version in telemetry data' do
85
+ expect(telemetry['version']).to eq(Minty::VERSION)
86
+ end
87
+
88
+ it 'should include the correct env in telemetry data' do
89
+ expect(telemetry['env']['ruby']).to eq(RUBY_VERSION)
90
+ end
91
+ end
92
+ end