cutting_edge 0.0.1 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
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