mingle_events 0.0.4

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 (41) hide show
  1. data/Gemfile +4 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.textile +117 -0
  4. data/lib/mingle_events.rb +26 -0
  5. data/lib/mingle_events/feed.rb +5 -0
  6. data/lib/mingle_events/feed/author.rb +28 -0
  7. data/lib/mingle_events/feed/category.rb +75 -0
  8. data/lib/mingle_events/feed/element_support.rb +19 -0
  9. data/lib/mingle_events/feed/entry.rb +96 -0
  10. data/lib/mingle_events/feed/page.rb +61 -0
  11. data/lib/mingle_events/http_error.rb +26 -0
  12. data/lib/mingle_events/mingle_basic_auth_access.rb +71 -0
  13. data/lib/mingle_events/mingle_oauth_access.rb +40 -0
  14. data/lib/mingle_events/poller.rb +32 -0
  15. data/lib/mingle_events/processors.rb +10 -0
  16. data/lib/mingle_events/processors/author_filter.rb +62 -0
  17. data/lib/mingle_events/processors/card_data.rb +102 -0
  18. data/lib/mingle_events/processors/card_type_filter.rb +28 -0
  19. data/lib/mingle_events/processors/category_filter.rb +19 -0
  20. data/lib/mingle_events/processors/custom_property_filter.rb +30 -0
  21. data/lib/mingle_events/processors/http_post_publisher.rb +20 -0
  22. data/lib/mingle_events/processors/pipeline.rb +20 -0
  23. data/lib/mingle_events/processors/puts_publisher.rb +17 -0
  24. data/lib/mingle_events/project_custom_properties.rb +33 -0
  25. data/lib/mingle_events/project_event_fetcher.rb +96 -0
  26. data/test/mingle_events/feed/author_test.rb +39 -0
  27. data/test/mingle_events/feed/category_test.rb +20 -0
  28. data/test/mingle_events/feed/entry_test.rb +140 -0
  29. data/test/mingle_events/feed/page_test.rb +82 -0
  30. data/test/mingle_events/poller_test.rb +47 -0
  31. data/test/mingle_events/processors/author_filter_test.rb +80 -0
  32. data/test/mingle_events/processors/card_data_test.rb +210 -0
  33. data/test/mingle_events/processors/card_type_filter_test.rb +51 -0
  34. data/test/mingle_events/processors/category_filter_test.rb +27 -0
  35. data/test/mingle_events/processors/custom_property_filter_test.rb +51 -0
  36. data/test/mingle_events/processors/pipeline_test.rb +32 -0
  37. data/test/mingle_events/project_custom_properties_test.rb +39 -0
  38. data/test/mingle_events/project_event_fetcher_test.rb +122 -0
  39. data/test/test_helper.rb +163 -0
  40. data/test/web_hook_server/web_hook_server.rb +6 -0
  41. metadata +140 -0
@@ -0,0 +1,27 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+ module Processors
5
+ class CategoryFilterTest < Test::Unit::TestCase
6
+
7
+ def test_removes_events_without_matching_categories
8
+ event_1 = stub_event(1, [Feed::Category::CARD, Feed::Category::COMMENT_ADDITION])
9
+ event_2 = stub_event(2, [Feed::Category::CARD, Feed::Category::PROPERTY_CHANGE])
10
+ event_3 = stub_event(3, [Feed::Category::REVISION_COMMIT])
11
+ event_4 = stub_event(4, [Feed::Category::CARD, Feed::Category::PROPERTY_CHANGE])
12
+ event_5 = stub_event(5, [])
13
+ events = [event_1, event_2, event_3, event_4, event_5]
14
+
15
+ filter = CategoryFilter.new([Feed::Category::CARD, Feed::Category::PROPERTY_CHANGE])
16
+ assert_equal([event_2, event_4], filter.process_events(events))
17
+ end
18
+
19
+ private
20
+
21
+ def stub_event(id, categories)
22
+ OpenStruct.new(:entry_id => id, :categories => categories)
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+ module Processors
5
+
6
+ class CustomPropertyFilterTest < Test::Unit::TestCase
7
+
8
+ def test_filters_events_on_custom_property
9
+ event_1 = stub_event(true)
10
+ event_2 = stub_event(false)
11
+ event_3 = stub_event(true)
12
+ event_4 = stub_event(true)
13
+ event_5 = stub_event(true)
14
+
15
+ card_data = {
16
+ event_1 => {:custom_properties => {'Priority' => 'High'}},
17
+ event_3 => {:custom_properties => {'Priority' => 'Low'}},
18
+ event_4 => {:custom_properties => {'Priority' => 'High'}},
19
+ event_5 => {:custom_properties => {'Severity' => 'High'}}
20
+ }
21
+ def card_data.for_card_event(card_event)
22
+ self[card_event]
23
+ end
24
+
25
+ filter = CustomPropertyFilter.new('Priority', 'High', card_data)
26
+ filtered_events = filter.process_events([event_1, event_2, event_3, event_4, event_5])
27
+ assert_equal([event_1, event_4], filtered_events)
28
+ end
29
+
30
+ def test_drops_events_for_deleted_cards
31
+ event_1 = stub_event(true)
32
+
33
+ card_data = {}
34
+ def card_data.for_card_event(card_event)
35
+ self[card_event]
36
+ end
37
+
38
+ filter = CustomPropertyFilter.new('Priority', 'High', card_data)
39
+ filtered_events = filter.process_events([event_1])
40
+ assert_equal([], filtered_events)
41
+ end
42
+
43
+ private
44
+
45
+ def stub_event(is_card)
46
+ OpenStruct.new(:card? => is_card)
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,32 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+ module Processors
5
+ class PipelineTest < Test::Unit::TestCase
6
+
7
+ def test_chains_all_processors
8
+ events = [stub_event(100), stub_event(101), stub_event(102)]
9
+ pipeline = Pipeline.new([DeleteLastProcessor.new, ReversingProcessor.new])
10
+ processed_events = pipeline.process_events(events)
11
+ assert_equal([101, 100], processed_events.map(&:entry_id))
12
+ end
13
+
14
+ def stub_event(id)
15
+ OpenStruct.new(:entry_id => id)
16
+ end
17
+
18
+ class ReversingProcessor
19
+ def process_events(events)
20
+ events.reverse
21
+ end
22
+ end
23
+
24
+ class DeleteLastProcessor
25
+ def process_events(events)
26
+ events[0..-2]
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+
5
+ class ProjectCustomPropertiesTest < Test::Unit::TestCase
6
+
7
+ def test_can_lookup_property_name_by_column_name
8
+ property_definitions_xml = %{
9
+ <?xml version="1.0" encoding="UTF-8"?>
10
+ <property_definitions type="array">
11
+ <property_definition>
12
+ <name>Account</name>
13
+ <data_type>string</data_type>
14
+ <is_numeric type="boolean">false</is_numeric>
15
+ <column_name>cp_account</column_name>
16
+ </property_definition>
17
+ <property_definition>
18
+ <name>Account ID</name>
19
+ <data_type>string</data_type>
20
+ <is_numeric type="boolean">false</is_numeric>
21
+ <column_name>cp_account_id</column_name>
22
+ </property_definition>
23
+ </property_definitions>
24
+ }
25
+
26
+ dummy_mingle_access = StubMingleAccess.new
27
+ dummy_mingle_access.register_page_content(
28
+ URI.escape('/api/v2/projects/atlas/property_definitions.xml'),
29
+ property_definitions_xml
30
+ )
31
+
32
+ project_custom_properties = ProjectCustomProperties.new(dummy_mingle_access, "atlas")
33
+ assert_equal("Account ID", project_custom_properties.property_name_for_column("cp_account_id"))
34
+
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,122 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+ class ProjectEventFetcherTest < Test::Unit::TestCase
5
+
6
+ def test_can_fetch_all_entries_and_write_to_disk_when_no_initial_state
7
+ state_dir = temp_dir
8
+ fetcher = ProjectEventFetcher.new('atlas', stub_mingle_access, state_dir)
9
+
10
+ file_for_first_new_entry = fetcher.fetch_latest
11
+
12
+ expected_entry_ids = [
13
+ 'https://mingle.example.com/projects/atlas/events/index/23',
14
+ 'https://mingle.example.com/projects/atlas/events/index/97',
15
+ 'https://mingle.example.com/projects/atlas/events/index/98',
16
+ 'https://mingle.example.com/projects/atlas/events/index/99',
17
+ 'https://mingle.example.com/projects/atlas/events/index/100',
18
+ 'https://mingle.example.com/projects/atlas/events/index/101',
19
+ 'https://mingle.example.com/projects/atlas/events/index/103'
20
+ ]
21
+
22
+ assert_expected_entry_chain_written_to_disk(expected_entry_ids, file_for_first_new_entry, fetcher)
23
+ end
24
+
25
+ def test_can_fetch_all_entries_and_write_to_disk_when_existing_state
26
+ state_dir = temp_dir
27
+ fetcher = ProjectEventFetcher.new('atlas', stub_mingle_access, state_dir)
28
+ setup_current_state(%{
29
+ <entry>
30
+ <id>https://mingle.example.com/projects/atlas/events/index/97</id>
31
+ <title>entry 97</title>
32
+ <updated>2011-02-03T01:10:52Z</updated>
33
+ <author><name>Harry</name></author>
34
+ </entry>}, fetcher)
35
+
36
+ file_for_first_new_entry = fetcher.fetch_latest
37
+
38
+ expected_entry_ids = [
39
+ 'https://mingle.example.com/projects/atlas/events/index/98',
40
+ 'https://mingle.example.com/projects/atlas/events/index/99',
41
+ 'https://mingle.example.com/projects/atlas/events/index/100',
42
+ 'https://mingle.example.com/projects/atlas/events/index/101',
43
+ 'https://mingle.example.com/projects/atlas/events/index/103'
44
+ ]
45
+
46
+ assert_expected_entry_chain_written_to_disk(expected_entry_ids, file_for_first_new_entry, fetcher)
47
+ end
48
+
49
+ def test_no_new_entries_with_current_state
50
+ state_dir = temp_dir
51
+ fetcher = ProjectEventFetcher.new('atlas', stub_mingle_access, state_dir)
52
+ setup_current_state(%{
53
+ <entry>
54
+ <id>https://mingle.example.com/projects/atlas/events/index/103</id>
55
+ <title>entry 103</title>
56
+ <updated>2011-02-03T01:10:52Z</updated>
57
+ <author><name>Harry</name></author>
58
+ </entry>}, fetcher)
59
+
60
+ assert_nil fetcher.fetch_latest
61
+
62
+ current_state = YAML.load(File.new(File.join(state_dir, 'current_state.yml')))
63
+ assert_equal(@last_fetched_entry_info_file, current_state[:last_fetched_entry_info_file])
64
+ end
65
+
66
+ def test_no_new_entries_with_no_current_state
67
+ state_dir = temp_dir
68
+ mingle_access = StubMingleAccess.new
69
+ mingle_access.register_page_content('/api/v2/projects/atlas/feeds/events.xml',%{
70
+ <?xml version="1.0" encoding="UTF-8"?>
71
+ <feed xmlns="http://www.w3.org/2005/Atom" xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
72
+ <title>Mingle Events: Blank Project</title>
73
+ <id>https://mingle.example.com/api/v2/projects/blank_project/feeds/events.xml</id>
74
+ <link href="https://mingle.example.com/api/v2/projects/blank_project/feeds/events.xml" rel="current"/>
75
+ <link href="https://mingle.example.com/api/v2/projects/blank_project/feeds/events.xml" rel="self"/>
76
+ <updated>2011-08-04T19:42:04Z</updated>
77
+ </feed>})
78
+ fetcher = ProjectEventFetcher.new('atlas', mingle_access, state_dir)
79
+
80
+ assert_nil fetcher.fetch_latest
81
+ assert !File.exist?(File.join(state_dir, 'current_state.yml'))
82
+ end
83
+
84
+ private
85
+
86
+ def setup_current_state(last_fetched_entry_xml, fetcher)
87
+ last_fetched_entry_info = {
88
+ :entry_xml => last_fetched_entry_xml,
89
+ :next_entry_file_path => nil
90
+ }
91
+ @last_fetched_entry_info_file = fetcher.file_for_entry(Feed::Entry.new(Nokogiri::XML(last_fetched_entry_xml).at('/entry')))
92
+ FileUtils.mkdir_p(File.dirname(@last_fetched_entry_info_file))
93
+ File.open(@last_fetched_entry_info_file, 'w') do |out|
94
+ YAML.dump(last_fetched_entry_info, out)
95
+ end
96
+ current_state = {:last_fetched_entry_info_file => @last_fetched_entry_info_file}
97
+ File.open(fetcher.current_state_file, 'w') do |out|
98
+ YAML.dump(current_state, out)
99
+ end
100
+
101
+ end
102
+
103
+ def assert_expected_entry_chain_written_to_disk(expected_entry_ids, file_for_first_new_entry, fetcher)
104
+
105
+ file_for_new_entry = file_for_first_new_entry
106
+ file_for_last_inspected_entry = nil
107
+ expected_entry_ids.each_with_index do |expected_entry_id, index|
108
+ file_for_last_inspected_entry = file_for_new_entry
109
+ new_entry_info = YAML.load(File.new(file_for_new_entry))
110
+ new_entry = Feed::Entry.new(Nokogiri::XML(new_entry_info[:entry_xml]).at('/entry'))
111
+ assert_equal(expected_entry_id, new_entry.entry_id)
112
+ file_for_new_entry = new_entry_info[:next_entry_file_path]
113
+ end
114
+
115
+ current_state = YAML.load(File.new(fetcher.current_state_file))
116
+ assert_equal(file_for_last_inspected_entry, current_state[:last_fetched_entry_info_file])
117
+
118
+ end
119
+
120
+
121
+ end
122
+ end
@@ -0,0 +1,163 @@
1
+ require 'test/unit'
2
+
3
+ require 'ostruct'
4
+ require 'fileutils'
5
+
6
+ require 'rubygems'
7
+ require 'active_support'
8
+
9
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'mingle_events'))
10
+
11
+ MingleEvents.log.level = Logger::WARN
12
+
13
+ class Test::Unit::TestCase
14
+
15
+ # page 3
16
+ LATEST_PAGE_CONTENT = %{
17
+ <feed xmlns="http://www.w3.org/2005/Atom" xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
18
+
19
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml" rel="current"/>
20
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml" rel="self"/>
21
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=2" rel="next"/>
22
+
23
+ <entry>
24
+ <id>https://mingle.example.com/projects/atlas/events/index/103</id>
25
+ <title>entry 103</title>
26
+ <updated>2011-02-03T08:12:42Z</updated>
27
+ <author><name>Bob</name></author>
28
+ </entry>
29
+ <entry>
30
+ <id>https://mingle.example.com/projects/atlas/events/index/101</id>
31
+ <title>entry 101</title>
32
+ <updated>2011-02-03T02:09:16Z</updated>
33
+ <author><name>Bob</name></author>
34
+ </entry>
35
+ <entry>
36
+ <id>https://mingle.example.com/projects/atlas/events/index/100</id>
37
+ <title>entry 100</title>
38
+ <updated>2011-02-03T01:58:02Z</updated>
39
+ <author><name>Mary</name></author>
40
+ </entry>
41
+ </feed>
42
+ }
43
+
44
+ # page 2
45
+ PAGE_2_CONTENT = %{
46
+ <feed xmlns="http://www.w3.org/2005/Atom" xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
47
+
48
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml" rel="current"/>
49
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=2" rel="self"/>
50
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=1" rel="next"/>
51
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=3" rel="previous"/>
52
+
53
+ <entry>
54
+ <id>https://mingle.example.com/projects/atlas/events/index/99</id>
55
+ <title>entry 99</title>
56
+ <updated>2011-02-03T01:30:52Z</updated>
57
+ <author><name>Harry</name></author>
58
+ </entry>
59
+
60
+ <entry>
61
+ <id>https://mingle.example.com/projects/atlas/events/index/98</id>
62
+ <title>entry 98</title>
63
+ <updated>2011-02-03T01:20:52Z</updated>
64
+ <author><name>Harry</name></author>
65
+ </entry>
66
+
67
+ <entry>
68
+ <id>https://mingle.example.com/projects/atlas/events/index/97</id>
69
+ <title>entry 97</title>
70
+ <updated>2011-02-03T01:10:52Z</updated>
71
+ <author><name>Harry</name></author>
72
+ </entry>
73
+ </feed>
74
+ }
75
+
76
+ # page 1
77
+ PAGE_1_CONTENT = %{
78
+ <feed xmlns="http://www.w3.org/2005/Atom" xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
79
+
80
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml" rel="current"/>
81
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=1" rel="self"/>
82
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=2" rel="previous"/>
83
+
84
+ <entry>
85
+ <id>https://mingle.example.com/projects/atlas/events/index/23</id>
86
+ <title>entry 23</title>
87
+ <updated>2011-02-01T01:00:52Z</updated>
88
+ <author><name>Bob</name></author>
89
+ </entry>
90
+ </feed>
91
+ }
92
+
93
+ def stub_mingle_access
94
+ stub = StubMingleAccess.new
95
+ stub.register_page_content('https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml', LATEST_PAGE_CONTENT)
96
+ stub.register_page_content('https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=2', PAGE_2_CONTENT)
97
+ stub.register_page_content('https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=1', PAGE_1_CONTENT)
98
+ stub.register_page_content('https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=3', LATEST_PAGE_CONTENT)
99
+ stub.register_page_content('/api/v2/projects/atlas/feeds/events.xml', LATEST_PAGE_CONTENT)
100
+ stub.register_page_content('/api/v2/projects/atlas/feeds/events.xml?page=2', PAGE_2_CONTENT)
101
+ stub.register_page_content('/api/v2/projects/atlas/feeds/events.xml?page=1', PAGE_1_CONTENT)
102
+ stub.register_page_content('/api/v2/projects/atlas/feeds/events.xml?page=3', LATEST_PAGE_CONTENT)
103
+
104
+ stub
105
+ end
106
+
107
+ def temp_dir
108
+ path = File.expand_path(File.join(File.dirname(__FILE__), 'tmp', ActiveSupport::SecureRandom.hex(16)))
109
+ FileUtils.mkdir_p(path)
110
+ path
111
+ end
112
+
113
+ def temp_file
114
+ File.join(temp_dir, ActiveSupport::SecureRandom.hex(16))
115
+ end
116
+
117
+ class StubMingleAccess
118
+
119
+ def initialize
120
+ @pages_by_path = {}
121
+ @not_found_pages = []
122
+ end
123
+
124
+ def base_url
125
+ 'http://example.com/mingle'
126
+ end
127
+
128
+ def register_page_content(path, content)
129
+ @pages_by_path[path] = content
130
+ end
131
+
132
+ def register_page_not_found(path)
133
+ @not_found_pages << path
134
+ end
135
+
136
+ def fetch_page(path)
137
+ if @not_found_pages.include?(path)
138
+ rsp = Net::HTTPNotFound.new(nil, '404', 'Page not found!')
139
+ def rsp.body
140
+ "404!!!!!"
141
+ end
142
+ raise MingleEvents::HttpError.new(rsp, path)
143
+ end
144
+
145
+ raise "Attempting to fetch page at #{path}, but your test has not registered content for this path! Registered paths: #{@pages_by_path.keys.inspect}" unless @pages_by_path.key?(path)
146
+ @pages_by_path[path]
147
+ end
148
+
149
+ end
150
+
151
+ class StubProjectCustomProperties
152
+
153
+ def initialize(property_names_by_column_names)
154
+ @property_names_by_column_names = property_names_by_column_names
155
+ end
156
+
157
+ def property_name_for_column(column_name)
158
+ @property_names_by_column_names[column_name]
159
+ end
160
+
161
+ end
162
+
163
+ end
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+
4
+ post '/' do
5
+ puts params[:event]
6
+ end