cutting_edge 0.0.1 → 0.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +7 -2
  3. data/Gemfile.lock +130 -0
  4. data/LICENSE +68 -81
  5. data/Procfile +1 -0
  6. data/README.md +272 -74
  7. data/Rakefile +0 -5
  8. data/bin/cutting_edge +60 -45
  9. data/config.rb +55 -0
  10. data/cutting_edge.gemspec +33 -4
  11. data/heroku.config.rb +20 -0
  12. data/lib/cutting_edge.rb +1 -1
  13. data/lib/cutting_edge/app.rb +95 -19
  14. data/lib/cutting_edge/langs.rb +7 -1
  15. data/lib/cutting_edge/langs/python.rb +5 -1
  16. data/lib/cutting_edge/langs/ruby.rb +5 -1
  17. data/lib/cutting_edge/langs/rust.rb +5 -1
  18. data/lib/cutting_edge/public/images/error.svg +24 -0
  19. data/lib/cutting_edge/public/images/languages/python.svg +1 -0
  20. data/lib/cutting_edge/public/images/languages/ruby.svg +1 -0
  21. data/lib/cutting_edge/public/images/languages/rust.svg +1 -0
  22. data/lib/cutting_edge/public/images/ok.svg +24 -0
  23. data/lib/cutting_edge/public/javascript/clipboard.min.js +7 -0
  24. data/lib/cutting_edge/public/javascript/cuttingedge.js +53 -0
  25. data/lib/cutting_edge/public/stylesheets/primer.css +22 -0
  26. data/lib/cutting_edge/repo.rb +124 -18
  27. data/lib/cutting_edge/templates/_footer.html.erb +3 -0
  28. data/lib/cutting_edge/templates/_header.html.erb +8 -0
  29. data/lib/cutting_edge/templates/_overview.html.erb +9 -0
  30. data/lib/cutting_edge/templates/badge.svg.erb +39 -0
  31. data/lib/cutting_edge/templates/index.html.erb +62 -0
  32. data/lib/cutting_edge/templates/info.html.erb +101 -0
  33. data/lib/cutting_edge/templates/mail.html.erb +163 -0
  34. data/lib/cutting_edge/workers/badge.rb +33 -11
  35. data/lib/cutting_edge/workers/dependency.rb +36 -16
  36. data/lib/cutting_edge/workers/helpers.rb +8 -0
  37. data/lib/cutting_edge/workers/mail.rb +38 -0
  38. data/projects.yml +25 -0
  39. data/spec/app_spec.rb +115 -0
  40. data/spec/badge_worker_spec.rb +77 -0
  41. data/spec/dependency_worker_spec.rb +132 -0
  42. data/spec/email_worker_spec.rb +43 -0
  43. data/spec/fixtures.rb +180 -0
  44. data/spec/fixtures/projects.yml +27 -0
  45. data/spec/langs/python_spec.rb +47 -5
  46. data/spec/langs/ruby_spec.rb +105 -0
  47. data/spec/langs/rust_spec.rb +31 -0
  48. data/spec/repo_spec.rb +52 -0
  49. data/spec/spec_helper.rb +9 -1
  50. metadata +43 -15
  51. data/lib/cutting_edge/badge.rb +0 -46
@@ -14,6 +14,14 @@ module WorkerHelpers
14
14
  def get_from_store(identifier)
15
15
  ::CuttingEdge::App.store[identifier]
16
16
  end
17
+
18
+ def badge_worker(identifier)
19
+ BadgeWorker.perform_async(identifier)
20
+ end
21
+
22
+ def mail_worker(identifier, to_address)
23
+ MailWorker.perform_async(identifier, to_address)
24
+ end
17
25
 
18
26
  end
19
27
 
@@ -0,0 +1,38 @@
1
+ require File.expand_path('../helpers.rb', __FILE__)
2
+ require 'erb'
3
+ require 'mail'
4
+
5
+ module CuttingEdge
6
+ MAIL_TEMPLATE = File.read(File.expand_path('../../templates/mail.html.erb', __FILE__)) unless defined?(MAIL_TEMPLATE)
7
+ end
8
+
9
+ class MailWorker < GenericWorker
10
+
11
+ def perform(identifier, to_addr)
12
+ log_info('Running Worker!')
13
+ dependencies = get_from_store(identifier)
14
+ unless to_addr && dependencies
15
+ log_info("Failed to execute email job for #{identifier}: #{dependencies ? dependencies : 'No dependencies found.'} #{'No e-mail address set.' if to_addr.nil?}")
16
+ return nil
17
+ end
18
+
19
+ Mail.deliver do
20
+ from "CuttingEdge <#{CuttingEdge::MAIL_FROM}>"
21
+ to to_addr
22
+ subject "Dependency Status Changed For #{identifier}"
23
+
24
+ text_part do
25
+ body "Dependency Status Update For #{identifier} By CuttingEdge"
26
+ end
27
+
28
+ html_part do
29
+ content_type 'text/html; charset=UTF-8'
30
+ body ERB.new(CuttingEdge::MAIL_TEMPLATE).result_with_hash(
31
+ project: identifier,
32
+ url: CuttingEdge::SERVER_URL,
33
+ specs: dependencies
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # Example projects.yml file for CuttingEdge
2
+ # For more information and options, see: https://github.com/repotag/cutting_edge
3
+
4
+ github:
5
+ repotag:
6
+ cutting_edge:
7
+ language: ruby
8
+ hide: mysecret
9
+ gollum:
10
+ gollum:
11
+ language: ruby
12
+ gollum-lib:
13
+ locations: [gemspec.rb]
14
+ dependency_types: [runtime, development]
15
+ singingwolfboy:
16
+ flask-dance:
17
+ language: python
18
+ email: false
19
+ rust-lang:
20
+ crates.io:
21
+ language: rust
22
+ gitlab:
23
+ cthowl01:
24
+ team-chess-ruby:
25
+ locations: [Gemfile]
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe CuttingEdgeHelpers do
4
+ it 'fails to load invalid projects.yml' do
5
+ expect(load_repositories('/tmp/fail.yml')).to be_nil
6
+ end
7
+ end
8
+
9
+ describe CuttingEdge::App do
10
+ before(:all) {
11
+ CuttingEdge::App.set(:repositories, load_repositories(File.expand_path('../fixtures/projects.yml', __FILE__)))
12
+ CuttingEdge::App.set(:store, Moneta.new(:Memory))
13
+ }
14
+ let(:app) { CuttingEdge::App.new }
15
+
16
+ context 'landing page' do
17
+ let(:response) { get '/' }
18
+ it 'returns status 200 OK' do
19
+ expect(response.status).to eq 200
20
+ end
21
+ end
22
+
23
+ context 'unknown project' do
24
+ let(:project) { 'github/doesnt/exist' }
25
+ ['/info', '/info/json', '/svg', '/refresh'].each do |route|
26
+ it "returns status 404 not found for #{route}" do
27
+ response = get("/#{project}#{route}")
28
+ expect(response.status).to eq 404
29
+ end
30
+ end
31
+ end
32
+
33
+ context 'known project' do
34
+ let(:project) { 'github/gollum/gollum' }
35
+ before {
36
+ add_to_store("svg-#{project}", 'test')
37
+ add_to_store(project, {'test' => true})
38
+ }
39
+ {'/info' => 'text/html;charset=utf-8', '/info/json' => 'application/json', '/svg' => 'image/svg+xml'}.each do |route, type|
40
+ it "returns status 200 OK for #{route} with mime #{type}" do
41
+ response = get("/#{project}#{route}")
42
+ expect(response.status).to eq 200
43
+ expect(response.content_type).to eq type
44
+ end
45
+ end
46
+
47
+ it 'returns status 500 for a known project json without data' do
48
+ response = get("/github/gollum/gollum-lib/info/json")
49
+ # the store contains nothing for gollum-lib
50
+ expect(response.status).to eq 500
51
+ end
52
+
53
+ it 'svgs have cache-control: no-cache' do
54
+ response = get("/#{project}/svg")
55
+ expect(response.headers['Cache-Control']).to eq 'no-cache'
56
+ end
57
+ end
58
+
59
+ context 'hidden repos' do
60
+ let(:project) { 'gitlab/gitlab-org/gitlab-foss' }
61
+ let(:repo) { CuttingEdge::App.repositories[project] }
62
+
63
+ it 'does not list hidden repos on the landing page' do
64
+ response = get('/')
65
+ expect(response.body).to include('team-chess-ruby')
66
+ expect(response.body).not_to include('gitlab-foss')
67
+ end
68
+
69
+ before {
70
+ ::CuttingEdge::SECRET_TOKEN = 'secret'
71
+ add_to_store(project, {'test' => true})
72
+ }
73
+ after {
74
+ CuttingEdge.send(:remove_const, :SECRET_TOKEN)
75
+ }
76
+
77
+ it 'returns a JSON encoded HTML partial if the token is correct' do
78
+ response = post('/hidden_repos', "{\"token\":\"secret\"}")
79
+ expect(response.body).to include('gitlab-foss')
80
+ expect(JSON.parse(response.body)['partial']).to include "<div class=\"Box\">"
81
+ expect(JSON.parse(response.body)['partial']).to include "gitlab/gitlab-org/gitlab-foss"
82
+ end
83
+
84
+ it 'only gives access to repo information with the repo hidden token' do
85
+ ['/info', '/info/json', '/svg'].each do |route|
86
+ url = File.join('/', project, route)
87
+ unauthorized_response = get(url)
88
+ expect(unauthorized_response.status).to eq 404
89
+ authorized_response = get(url, token: repo.hidden_token)
90
+ expect(authorized_response.status).to eq 200
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ context 'refreshing' do
97
+ let(:project) { 'github/gollum/gollum' }
98
+ before {
99
+ ::CuttingEdge::SECRET_TOKEN = 'secret'
100
+ }
101
+ after {
102
+ CuttingEdge.send(:remove_const, :SECRET_TOKEN)
103
+ }
104
+
105
+ it 'fails with wrong token' do
106
+ response = post "/#{project}/refresh", :token => 'fail'
107
+ expect(response.status).to eq 401
108
+ end
109
+ it 'succeeds with right token' do
110
+ expect(DependencyWorker).to receive(:perform_async).exactly(:once)
111
+ response = post "/#{project}/refresh", token: 'secret'
112
+ expect(response.status).to eq 200
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,77 @@
1
+ standard_svg = <<EOF
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="20" role="img" aria-label="CuttingEdge Dependency Status">
3
+ <title>CuttingEdge Dependency Status</title>
4
+ <linearGradient id="s" x2="0" y2="100%">
5
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
6
+ <stop offset="1" stop-opacity=".1"/>
7
+ </linearGradient>
8
+ <clipPath id="r">
9
+ <rect width="100" height="20" rx="3" fill="#fff"/>
10
+ </clipPath>
11
+ <g clip-path="url(#r)">
12
+ <rect width="35" height="20" fill="#555"/>
13
+
14
+ <rect x="25" width="25" height="20" fill="#4c1"/>
15
+
16
+ <rect x="50" width="25" height="20" fill="#fe7d37"/>
17
+
18
+ <rect x="75" width="25" height="20" fill="#e05d44"/>
19
+
20
+ <rect width="100" height="20" fill="url(#s)"/>
21
+ </g>
22
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
23
+
24
+ <text aria-hidden="true" x="125" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)">CE</text>
25
+ <text x="125" y="140" transform="scale(.1)" fill="#fff">CE</text>
26
+
27
+
28
+ <g>
29
+ <title>Ok: 3</title>
30
+ <text aria-hidden="true" x="370" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)">3</text>
31
+ <text x="370" y="140" transform="scale(.1)" fill="#fff">3</text>
32
+ </g>
33
+
34
+ <g>
35
+ <title>Outdated Minor: 1</title>
36
+ <text aria-hidden="true" x="620" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)">1</text>
37
+ <text x="620" y="140" transform="scale(.1)" fill="#fff">1</text>
38
+ </g>
39
+
40
+ <g>
41
+ <title>Outdated Major: 3</title>
42
+ <text aria-hidden="true" x="870" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)">3</text>
43
+ <text x="870" y="140" transform="scale(.1)" fill="#fff">3</text>
44
+ </g>
45
+
46
+ </g>
47
+ </svg>
48
+ EOF
49
+ describe BadgeWorker do
50
+ let(:worker) { BadgeWorker.new }
51
+ let(:identifier) { 'github/gollum/gollum' }
52
+
53
+ let(:dependencies) {
54
+ mock_dependencies('gollum')
55
+ }
56
+
57
+ context 'performing' do
58
+
59
+ it 'generates an svg for outdated dependencies' do
60
+ expect(worker).to receive(:get_from_store).with(identifier).and_return(dependencies)
61
+ expect(worker).to receive(:add_to_store).with("svg-#{identifier}", standard_svg.chomp).and_return(true)
62
+ worker.perform(identifier)
63
+ end
64
+
65
+ it 'shows an error badge when there are no dependencies' do
66
+ expect(worker).to receive(:get_from_store).with(identifier).and_return(nil)
67
+ expect(worker).to receive(:add_to_store).with("svg-#{identifier}", CuttingEdge::BADGE_ERROR).and_return(true)
68
+ worker.perform(identifier)
69
+ end
70
+
71
+ it 'shows an ok badge when up to date' do
72
+ expect(worker).to receive(:get_from_store).with(identifier).and_return(mock_dependencies('ok'))
73
+ expect(worker).to receive(:add_to_store).with("svg-#{identifier}", CuttingEdge::BADGE_OK).and_return(true)
74
+ worker.perform(identifier)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,132 @@
1
+ class MockResponse
2
+ def initialize(status, body)
3
+ @status = status
4
+ @body = body
5
+ end
6
+
7
+ attr_reader :status
8
+
9
+ def to_s
10
+ @body
11
+ end
12
+ end
13
+
14
+ describe DependencyWorker do
15
+ let(:worker) { DependencyWorker.new }
16
+ let(:identifier) { 'github/gollum/gollum' }
17
+ let(:test_email) { 'cutting_edge@localhost' }
18
+ let(:lang) { 'ruby' }
19
+ let(:locations) {
20
+ {
21
+ 'gollum.gemspec' => 'http://github.bla/gollum/gollum/gollum.gemspec',
22
+ 'Gemfile' => 'http://github.bla/gollum/gollum/Gemfile'
23
+ }
24
+ }
25
+ let(:dependency_types) { [:runtime] }
26
+ let(:old_dependencies) {
27
+ mock_dependencies('gollum')
28
+ }
29
+ let(:new_dependencies) {
30
+ mock_dependencies('gollum-updated')
31
+ }
32
+
33
+ context 'http fetching files' do
34
+ let(:response_ok) { MockResponse.new(200, 'body') }
35
+ let(:response_not_found) { MockResponse.new(404, 'Not found') }
36
+ let(:url) { locations.first.last }
37
+ before {
38
+ worker.instance_variable_set(:@provider, ::CuttingEdge::GitlabRepository)
39
+ }
40
+
41
+ it 'uses default headers' do
42
+ worker.instance_variable_set(:@provider, ::CuttingEdge::GithubRepository)
43
+ expect(HTTP).to receive(:headers).with({:accept => 'application/vnd.github.v3.raw'}).exactly(:once).and_call_original
44
+ expect_any_instance_of(HTTP::Client).to receive(:get).with(url).and_return(response_ok)
45
+ expect(worker.send(:http_get, url)).to eq 'body'
46
+ end
47
+
48
+ it 'returns body when http get is successful' do
49
+ expect_any_instance_of(HTTP::Client).to receive(:get).with(url).and_return(response_ok)
50
+ expect(worker.send(:http_get, url)).to eq 'body'
51
+ end
52
+
53
+ it 'uses an auth header when it is set for project' do
54
+ token = 'token'
55
+ header = "Bearer #{token}"
56
+ worker.instance_variable_set(:@auth_token, token)
57
+ expect(::CuttingEdge::GitlabRepository).to receive(:headers).with(token).exactly(:once).and_call_original
58
+ expect(HTTP).to receive(:headers).with(:authorization => 'Bearer token').exactly(:once).and_call_original
59
+ expect_any_instance_of(HTTP::Client).to receive(:get).with(url).and_return(response_ok)
60
+ expect(worker.send(:http_get, url)).to eq 'body'
61
+ end
62
+
63
+ it 'returns body when http get is unsuccessful' do
64
+ expect_any_instance_of(HTTP::Client).to receive(:get).with(url).and_return(response_not_found)
65
+ expect(worker.send(:http_get, url)).to be_nil
66
+ end
67
+
68
+ it 'handles a timeout' do
69
+ expect_any_instance_of(HTTP::Client).to receive(:get).with(url).and_raise(HTTP::TimeoutError)
70
+ expect(worker).to receive(:log_info)
71
+ expect(worker.send(:http_get, url)).to be_nil
72
+ end
73
+ end
74
+
75
+ it 'generates results for fetched requirements' do
76
+ worker.instance_variable_set(:@lang, RubyLang)
77
+ fetched = [RubyLang.unknown_dependency('unknown_requirement'), Gem::Version.new('1.0')]
78
+ result_no_requirement = {:no_requirement=>[{:latest=>"1.0", :name=>"unknown_requirement", :required=>"unknown", :type=>:runtime, :url=>nil}], :ok=>[], :outdated_major=>[], :outdated_minor=>[], :outdated_patch=>[], :unknown=>[]}
79
+ expect(worker.send(:get_results, [fetched], dependency_types)).to eq result_no_requirement
80
+
81
+ fetched = [Gem::Dependency.new('unknown_version', '1.0', :runtime), nil]
82
+ result_no_version = {:no_requirement=>[], :ok=>[], :outdated_major=>[], :outdated_minor=>[], :outdated_patch=>[], :unknown=>[{:latest=>"", :name=>"unknown_version", :required=>"= 1.0", :type=>:runtime, :url=>"https://rubygems.org/gems/unknown_version"}]}
83
+ expect(worker.send(:get_results, [fetched], dependency_types)).to eq result_no_version
84
+ end
85
+
86
+ context 'performing' do
87
+
88
+ before(:each) {
89
+ expect(worker).to receive(:get_from_store).with(identifier).and_return(old_dependencies)
90
+ expect(worker).to receive(:http_get).and_return('fake').exactly(locations.length).times
91
+ }
92
+
93
+ context 'when the dependencies have changed' do
94
+
95
+ before(:each) {
96
+ locations.each_key do |loc|
97
+ expect(RubyLang).to receive(:parse_file).with(loc, 'fake').and_return(mock_fetched_requirements('gollum', loc, true))
98
+ end
99
+ expect(worker).to receive(:badge_worker).with(identifier).and_return(true)
100
+ expect(worker).to receive(:mail_worker).with(identifier, test_email).and_return(true)
101
+ }
102
+
103
+ it 'updates the store with newest dependencies' do
104
+ expect(worker).to receive(:add_to_store).with(identifier, new_dependencies).and_return(true)
105
+ expect(worker.instance_variable_get(:@nothing_changed)).to be_nil
106
+ worker.perform(identifier, lang, locations, dependency_types, test_email)
107
+ expect(worker.instance_variable_get(:@nothing_changed)).to be false
108
+ end
109
+
110
+ end
111
+
112
+ context 'when the dependencies have not changed' do
113
+
114
+ before(:each) {
115
+ locations.each_key do |loc|
116
+ expect(RubyLang).to receive(:parse_file).with(loc, 'fake').and_return(mock_fetched_requirements('gollum', loc, false))
117
+ end
118
+ expect(worker).not_to receive(:badge_worker)
119
+ expect(worker).not_to receive(:mail_worker)
120
+ }
121
+
122
+ it 'does not update the store' do
123
+ expect(worker).not_to receive(:add_to_store)
124
+ expect(worker.instance_variable_get(:@nothing_changed)).to be_nil
125
+ worker.perform(identifier, lang, locations, dependency_types, test_email)
126
+ expect(worker.instance_variable_get(:@nothing_changed)).to be true
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,43 @@
1
+ require 'mail'
2
+ Mail.defaults do
3
+ delivery_method :test
4
+ end
5
+
6
+ describe MailWorker do
7
+ let(:worker) { MailWorker.new }
8
+ let(:identifier) { 'github/repotag/cutting_edge' }
9
+ let(:test_email) { 'cutting_edge@localhost' }
10
+ let(:dependencies) {
11
+ mock_dependencies('gollum')
12
+ }
13
+
14
+ before(:each) {
15
+ expect(worker).to receive(:get_from_store).with(identifier).and_return(dependencies)
16
+ }
17
+
18
+ it 'returns nil when to address is nil ' do
19
+ expect(worker.perform(identifier, nil)).to eq nil
20
+ end
21
+
22
+ it 'sends an update mail' do
23
+ expect(Mail::TestMailer.deliveries).to be_empty
24
+ worker.perform(identifier, test_email)
25
+ expect(Mail::TestMailer.deliveries).to_not be_empty
26
+
27
+ mail = Mail::TestMailer.deliveries.first
28
+ expect(mail.from.first).to eq 'cutting_edge@localhost'
29
+ expect(mail.to.first).to eq test_email
30
+ expect(mail.subject).to eq "Dependency Status Changed For #{identifier}"
31
+
32
+ body = mail.body
33
+ expect(body.parts.length).to eq 2
34
+
35
+ html_body = body.parts.last.to_s
36
+ expect(html_body).to start_with('Content-Type: text/html')
37
+ expect(html_body).to include('This is <a href="http://localhost">CuttingEdge</a> informing you')
38
+ expect(html_body).to include("<a href=\"http://localhost/#{identifier}/info\">#{identifier}</a>")
39
+ expect(html_body).to include('In <b>gollum.gemspec</b>:')
40
+ expect(html_body).to include('<b>Outdated Major</b>:')
41
+ expect(html_body).to include('<li>rake ~> 12.3, >= 12.3.3 (latest: 13.0.1)</li>')
42
+ end
43
+ end