redmine-mattermost 0.3 → 0.4.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +15 -0
  3. data/LICENSE +1 -0
  4. data/README.md +30 -17
  5. data/Rakefile +9 -0
  6. data/app/views/settings/_mattermost_settings.html.erb +60 -20
  7. data/lib/redmine-mattermost.rb +9 -1
  8. data/lib/redmine_mattermost/bridge.rb +63 -0
  9. data/lib/redmine_mattermost/client.rb +28 -0
  10. data/lib/redmine_mattermost/extractors.rb +88 -0
  11. data/lib/redmine_mattermost/extractors/applied_changeset.rb +55 -0
  12. data/lib/redmine_mattermost/extractors/changed_wiki_page.rb +39 -0
  13. data/lib/redmine_mattermost/extractors/new_issue.rb +44 -0
  14. data/lib/redmine_mattermost/extractors/updated_issue.rb +42 -0
  15. data/lib/redmine_mattermost/infos.rb +1 -2
  16. data/lib/redmine_mattermost/issue_patch.rb +14 -14
  17. data/lib/redmine_mattermost/listener.rb +38 -268
  18. data/lib/redmine_mattermost/message_builder.rb +61 -0
  19. data/lib/redmine_mattermost/redmine_plugin.rb +3 -4
  20. data/lib/redmine_mattermost/utils.rb +3 -0
  21. data/lib/redmine_mattermost/utils/journal_mapper.rb +75 -0
  22. data/lib/redmine_mattermost/utils/text.rb +37 -0
  23. data/lib/redmine_mattermost/utils/urls.rb +20 -0
  24. data/lib/redmine_mattermost/version.rb +1 -1
  25. data/redmine-mattermost.gemspec +4 -1
  26. data/spec/client_spec.rb +29 -0
  27. data/spec/extractors/applied_changeset_spec.rb +82 -0
  28. data/spec/extractors/changed_wiki_page_spec.rb +63 -0
  29. data/spec/extractors/new_issue_spec.rb +209 -0
  30. data/spec/extractors/updated_issue_spec.rb +169 -0
  31. data/spec/message_builder_spec.rb +71 -0
  32. data/spec/redmine_mattermost_spec.rb +9 -0
  33. data/spec/spec_helper.rb +17 -0
  34. data/spec/support/mock_support.rb +113 -0
  35. data/spec/utils/text_spec.rb +142 -0
  36. data/spec/utils/text_spec_alt.rb +142 -0
  37. metadata +87 -9
@@ -0,0 +1,61 @@
1
+ module RedmineMattermost
2
+ class MessageBuilder
3
+ DEFAULTS = {
4
+ link_names: 1
5
+ }
6
+
7
+ def initialize(message)
8
+ @data = DEFAULTS.merge(text: message)
9
+ @attachments = []
10
+ end
11
+
12
+ def to_hash
13
+ @data.dup.tap do |hash|
14
+ unless @attachments.empty?
15
+ hash[:attachments] = @attachments.map(&:to_hash)
16
+ end
17
+ end
18
+ end
19
+
20
+ def username(name)
21
+ @data[:username] = name
22
+ end
23
+
24
+ def channel(id)
25
+ @data[:channel] = id
26
+ end
27
+
28
+ def icon(url)
29
+ @data[:icon_url] = url unless url.nil? || url.empty?
30
+ end
31
+
32
+ def attachment
33
+ Attachment.new.tap do |am|
34
+ @attachments << am
35
+ end
36
+ end
37
+
38
+ class Attachment
39
+ def initialize()
40
+ @data = { }
41
+ end
42
+
43
+ def text(message)
44
+ @data[:text] = message
45
+ self
46
+ end
47
+
48
+ def field(title, value, short = false)
49
+ {title: title, value: value}.tap do |hash|
50
+ hash[:short] = true if short
51
+ (@data[:fields] ||= []) << hash
52
+ end
53
+ self
54
+ end
55
+
56
+ def to_hash
57
+ @data.to_hash
58
+ end
59
+ end
60
+ end
61
+ end
@@ -6,9 +6,9 @@ module RedmineMattermost
6
6
  DEFAULT_SETTINGS = {
7
7
  "callback_url" => "http://example.com/callback/",
8
8
  "channel" => nil,
9
- "icon" => "https://raw.githubusercontent.com/altsol/redmine_mattermost/assets/icon.png",
10
- "username" => "redmine",
11
- "display_watchers" => "no"
9
+ "icon" => "https://raw.githubusercontent.com/neopoly/redmine-mattermost/assets/icon.png",
10
+ "username" => "Redmine",
11
+ "display_watchers" => "0"
12
12
  }.freeze
13
13
 
14
14
  SETTING_PARTIAL = "settings/mattermost_settings"
@@ -27,7 +27,6 @@ module RedmineMattermost
27
27
  description DESCRIPTION
28
28
  version VERSION
29
29
  url URL
30
- author_url AUTHOR_URL
31
30
  directory Engine.root
32
31
 
33
32
  requires_redmine version_or_higher: "2.0.0"
@@ -0,0 +1,3 @@
1
+ require "redmine_mattermost/utils/text"
2
+ require "redmine_mattermost/utils/urls"
3
+ require "redmine_mattermost/utils/journal_mapper"
@@ -0,0 +1,75 @@
1
+ module RedmineMattermost
2
+ module Utils
3
+ class JournalMapper
4
+ include Text
5
+ include Urls
6
+
7
+ attr_reader :bridge
8
+
9
+ def initialize(bridge)
10
+ @bridge = bridge
11
+ end
12
+
13
+ def to_fields(journal)
14
+ journal.details.map do |detail|
15
+ detail_to_field(detail)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def detail_to_field(detail)
22
+ short = true
23
+ value = h(detail.value)
24
+
25
+ if detail.property == "cf"
26
+ key = bridge.find_custom_field(detail.prop_key).name rescue nil
27
+ title = key
28
+ elsif detail.property == "attachment"
29
+ key = "attachment"
30
+ title = t("label_attachment")
31
+ else
32
+ key = detail.prop_key.to_s.sub("_id", "")
33
+ title = t("field_#{key}")
34
+ end
35
+
36
+ case key
37
+ when "title", "subject", "description"
38
+ short = false
39
+ when "tracker"
40
+ tracker = bridge.find_tracker(detail.value)
41
+ value = h tracker.to_s
42
+ when "project"
43
+ project = bridge.find_project(detail.value)
44
+ value = h project.to_s
45
+ when "status"
46
+ status = bridge.find_issue_status(detail.value)
47
+ value = h status.to_s
48
+ when "priority"
49
+ priority = bridge.find_issue_priority(detail.value)
50
+ value = h priority.to_s
51
+ when "category"
52
+ category = bridge.find_issue_category(detail.value)
53
+ value = h category.to_s
54
+ when "assigned_to"
55
+ user = bridge.find_user(detail.value)
56
+ value = h user.to_s
57
+ when "fixed_version"
58
+ version = bridge.find_version(detail.value)
59
+ value = h version.to_s
60
+ when "attachment"
61
+ if attachment = bridge.find_attachement(detail.prop_key)
62
+ value = "<#{event_url attachment}|#{h attachment.filename}>"
63
+ end
64
+ when "parent"
65
+ issue = bridge.find_issue(detail.value)
66
+ value = "<#{event_url issue}|#{h issue}>" if issue
67
+ end
68
+
69
+ value = "-" if value.nil? || value.empty?
70
+
71
+ [title, value, short]
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,37 @@
1
+ require "reverse_markdown"
2
+
3
+ module RedmineMattermost
4
+ module Utils
5
+ module Text
6
+ MARKDOWN_OPTIONS = {
7
+ unknown_tags: :bypass,
8
+ github_flavored: true
9
+ }.freeze
10
+
11
+ def h(text)
12
+ text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") if text
13
+ end
14
+
15
+ def t(key, options = {})
16
+ opts = {locale: bridge.settings.default_language}.merge(options)
17
+ bridge.translate(key, opts)
18
+ end
19
+
20
+ def link(label, target)
21
+ "<#{target}|#{h label}>"
22
+ end
23
+
24
+ def to_markdown(text)
25
+ case formatting = bridge.settings.text_formatting
26
+ when "markdown"
27
+ text
28
+ else
29
+ html = bridge.to_html(formatting, text)
30
+ ReverseMarkdown.convert(html, MARKDOWN_OPTIONS).strip
31
+ end
32
+ rescue
33
+ h text
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ module RedmineMattermost
2
+ module Utils
3
+ module Urls
4
+ def default_url_options
5
+ settings = bridge.settings
6
+ protocol = settings.protocol
7
+ if settings.host_name.to_s =~ /\A(https?\:\/\/)?(.+?)(\:(\d+))?(\/.+)?\z/i
8
+ host, port, prefix = $2, $4, $5
9
+ { host: host, protocol: protocol, port: port, script_name: prefix }
10
+ else
11
+ { host: settings.host_name, protocol: settings.protocol }
12
+ end
13
+ end
14
+
15
+ def event_url(obj)
16
+ bridge.url_for(obj.event_url(default_url_options))
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module RedmineMattermost
2
- VERSION = "0.3"
2
+ VERSION = "0.4.beta1"
3
3
  end
@@ -19,8 +19,11 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "rails", "~> 4.2.0"
22
- spec.add_dependency "httpclient", "~> 2.8.2"
22
+ spec.add_dependency "reverse_markdown", "~> 1.0.3"
23
23
 
24
24
  spec.add_development_dependency "bundler", "~> 1.7"
25
25
  spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "minitest", "~> 5.9.0"
27
+ spec.add_development_dependency "webmock", "~> 2.1.0"
28
+ spec.add_development_dependency "RedCloth", "~> 4.3.2"
26
29
  end
@@ -0,0 +1,29 @@
1
+ require "spec_helper"
2
+
3
+ describe RedmineMattermost::Client do
4
+ subject { RedmineMattermost::Client.new(url) }
5
+ let(:url) { "http://test.host/api/v3" }
6
+
7
+ it "use the given url as endpoint" do
8
+ subject.uri.must_equal URI(url)
9
+ end
10
+
11
+ it "speak the message as payload" do
12
+ message = {
13
+ text: "Some message",
14
+ username: "TestUser"
15
+ }
16
+
17
+ stub_endpoint
18
+ .with(body: { payload: message.to_json })
19
+ .to_return(body: "{}")
20
+
21
+ subject.speak(message).join
22
+ end
23
+
24
+ private
25
+
26
+ def stub_endpoint
27
+ stub_request(:post, url)
28
+ end
29
+ end
@@ -0,0 +1,82 @@
1
+ require "spec_helper"
2
+
3
+ describe RedmineMattermost::Extractors::AppliedChangeset do
4
+ subject { RedmineMattermost::Extractors::AppliedChangeset.new(bridge) }
5
+ let(:result) { subject.from_context(context) }
6
+ let(:msg) { result[:message] }
7
+ let(:context) do
8
+ { issue: issue, changeset: changeset }
9
+ end
10
+ let(:bridge) { MockBridge.new(settings) }
11
+ let(:issue) { MockIssue.new(issue_data) }
12
+ let(:journal) { mock(journal_data) }
13
+ let(:changeset) { mock(changeset_data)}
14
+ let(:settings) { Defaults.settings }
15
+
16
+ let(:issue_data) do
17
+ {
18
+ title: "Some title",
19
+ project: EventMock.new(title: "Project A"),
20
+ current_journal: journal,
21
+ valid: true
22
+ }
23
+ end
24
+ let(:journal_data) do
25
+ {
26
+ user: "User A",
27
+ details: [mock(prop_key: "title", value: "Some title")]
28
+ }
29
+ end
30
+ let(:changeset_data) do
31
+ {
32
+ repository: mock({
33
+ project: EventMock.new(title: "Project A"),
34
+ identifier_param: "repo_1",
35
+ }),
36
+ revision: "revision_1"
37
+ }
38
+ end
39
+
40
+ describe "empty context" do
41
+ let(:context) { Hash.new }
42
+
43
+ it "raises" do
44
+ proc { result }.must_raise KeyError
45
+ end
46
+ end
47
+
48
+ describe "no project" do
49
+ let(:issue_data) do
50
+ { title: "Some title" }
51
+ end
52
+
53
+ it "returns nil" do
54
+ result.must_be_nil
55
+ end
56
+ end
57
+
58
+ describe "invalid issue" do
59
+ let(:issue_data) do
60
+ { valid: false }
61
+ end
62
+ end
63
+
64
+ describe "with changeset" do
65
+ it "should return a message" do
66
+ msg[:text].must_equal(
67
+ "[#{link_for("Project A")}] User A updated #{link_for("Some title")}"
68
+ )
69
+ attachments = msg[:attachments]
70
+ attachments.size.must_equal 1
71
+ attachment = attachments.shift
72
+ text = attachment[:text]
73
+ text.must_equal("text_status_changed_by_changeset")
74
+ fields = attachment[:fields]
75
+ fields.size.must_equal 1
76
+ fields.shift.must_equal({
77
+ title: "field_title",
78
+ value: "Some title"
79
+ })
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,63 @@
1
+ require "spec_helper"
2
+
3
+ describe RedmineMattermost::Extractors::ChangedWikiPage do
4
+ subject { RedmineMattermost::Extractors::ChangedWikiPage.new(bridge) }
5
+ let(:result) { subject.from_context(context) }
6
+ let(:msg) { result[:message] }
7
+ let(:context) do
8
+ { project: project, page: page }
9
+ end
10
+ let(:bridge) { MockBridge.new(settings) }
11
+ let(:project) { EventMock.new(title: "Project A")}
12
+ let(:page) { EventMock.new(page_data)}
13
+
14
+ let(:settings) { Defaults.settings }
15
+ let(:page_data) do
16
+ {
17
+ title: "Some title",
18
+ content: mock({
19
+ author: mock(title: "User A"),
20
+ comments: ""
21
+ })
22
+ }
23
+ end
24
+
25
+ describe "empty context" do
26
+ let(:context) { Hash.new }
27
+
28
+ it "raises" do
29
+ proc { result }.must_raise KeyError
30
+ end
31
+ end
32
+
33
+ describe "minimal page" do
34
+ it "should return a message" do
35
+ msg[:text].must_equal(
36
+ "[#{link_for("Project A")}] User A updated #{link_for("Some title")}"
37
+ )
38
+ end
39
+ end
40
+
41
+ describe "full page" do
42
+ let(:page_data) do
43
+ {
44
+ title: "Some title",
45
+ content: mock({
46
+ author: mock(title: "User A"),
47
+ comments: "The comments"
48
+ })
49
+ }
50
+ end
51
+
52
+ it "should return a message" do
53
+ msg[:text].must_equal(
54
+ "[#{link_for("Project A")}] User A updated #{link_for("Some title")}"
55
+ )
56
+ attachments = msg[:attachments]
57
+ attachments.size.must_equal 1
58
+ attachment = attachments.shift
59
+ text = attachment[:text]
60
+ text.must_equal "The comments"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,209 @@
1
+ require "spec_helper"
2
+
3
+ describe RedmineMattermost::Extractors::NewIssue do
4
+ subject { RedmineMattermost::Extractors::NewIssue.new(bridge) }
5
+ let(:result) { subject.from_context(context) }
6
+ let(:msg) { result[:message] }
7
+ let(:context) do
8
+ { issue: issue }
9
+ end
10
+ let(:bridge) { MockBridge.new(settings) }
11
+ let(:settings) { Defaults.settings }
12
+ let(:issue) { MockIssue.new(issue_data) }
13
+ let(:issue_data) { Hash.new }
14
+
15
+ describe "empty context" do
16
+ let(:context) { Hash.new }
17
+
18
+ it "raises" do
19
+ proc { result }.must_raise KeyError
20
+ end
21
+ end
22
+
23
+ describe "no project" do
24
+ let(:issue_data) do
25
+ {
26
+ title: "Some title"
27
+ }
28
+ end
29
+
30
+ it "returns nil" do
31
+ result.must_be_nil
32
+ end
33
+ end
34
+
35
+ describe "minimal issue" do
36
+ let(:issue_data) do
37
+ {
38
+ title: "Some title",
39
+ project: EventMock.new(title: "Project A"),
40
+ author: mock(title: "User A"),
41
+ watcher_users: [],
42
+ status: "",
43
+ priority: "",
44
+ assigned_to: nil
45
+ }
46
+ end
47
+
48
+ it "should return a message" do
49
+ msg[:text].must_equal(
50
+ "[#{link_for("Project A")}] User A created #{link_for("Some title")}"
51
+ )
52
+ attachments = msg[:attachments]
53
+ attachments.size.must_equal 1
54
+ attachment = attachments.shift
55
+ fields = attachment[:fields]
56
+ fields.size.must_equal 3
57
+ fields.shift.must_equal({
58
+ title: "field_status",
59
+ value: "-",
60
+ short: true
61
+ })
62
+ fields.shift.must_equal({
63
+ title: "field_priority",
64
+ value: "-",
65
+ short: true
66
+ })
67
+ fields.shift.must_equal({
68
+ title: "field_assigned_to",
69
+ value: "-",
70
+ short: true
71
+ })
72
+ end
73
+ end
74
+
75
+ describe "default issue" do
76
+ let(:issue_data) do
77
+ {
78
+ title: "Some title",
79
+ project: EventMock.new(title: "Project A"),
80
+ author: mock(title: "User A"),
81
+ watcher_users: [],
82
+ status: "New",
83
+ priority: "Normal",
84
+ assigned_to: mock(title: "User B")
85
+ }
86
+ end
87
+
88
+ it "should return a message" do
89
+ msg[:text].must_equal(
90
+ "[#{link_for("Project A")}] User A created #{link_for("Some title")}"
91
+ )
92
+ attachments = msg[:attachments]
93
+ attachments.size.must_equal 1
94
+ attachment = attachments.shift
95
+ fields = attachment[:fields]
96
+ fields.size.must_equal 3
97
+ fields.shift.must_equal({
98
+ title: "field_status",
99
+ value: "New",
100
+ short: true
101
+ })
102
+ fields.shift.must_equal({
103
+ title: "field_priority",
104
+ value: "Normal",
105
+ short: true
106
+ })
107
+ fields.shift.must_equal({
108
+ title: "field_assigned_to",
109
+ value: "User B",
110
+ short: true
111
+ })
112
+ end
113
+ end
114
+
115
+ describe "full issue" do
116
+ let(:issue_data) do
117
+ {
118
+ title: "Some title",
119
+ project: EventMock.new(title: "Project A"),
120
+ author: mock(title: "User A"),
121
+ watcher_users: [mock(title: "User C")],
122
+ status: "New",
123
+ priority: "Normal",
124
+ assigned_to: mock(title: "User B"),
125
+ description: <<TEXTILE
126
+ Some description with @inline@ and
127
+
128
+ <pre>pre-text
129
+ over multiple lines
130
+ </pre>
131
+
132
+ <code>
133
+ and code
134
+ </code>
135
+ TEXTILE
136
+ }
137
+ end
138
+
139
+ it "should return a message" do
140
+ msg[:text].must_equal(
141
+ "[#{link_for("Project A")}] User A created #{link_for("Some title")}"
142
+ )
143
+ attachments = msg[:attachments]
144
+ attachments.size.must_equal 1
145
+ attachment = attachments.shift
146
+
147
+ text = attachment[:text]
148
+ text.must_equal <<MARKDOWN.strip
149
+ Some description with `inline` and
150
+
151
+ ```
152
+ pre-text
153
+ over multiple lines
154
+ ```
155
+
156
+ `
157
+ and code
158
+ `
159
+ MARKDOWN
160
+ fields = attachment[:fields]
161
+ fields.size.must_equal 4
162
+ fields.shift.must_equal({
163
+ title: "field_status",
164
+ value: "New",
165
+ short: true
166
+ })
167
+ fields.shift.must_equal({
168
+ title: "field_priority",
169
+ value: "Normal",
170
+ short: true
171
+ })
172
+ fields.shift.must_equal({
173
+ title: "field_assigned_to",
174
+ value: "User B",
175
+ short: true
176
+ })
177
+ fields.shift.must_equal({
178
+ title: "field_watcher",
179
+ value: "User C",
180
+ short: true
181
+ })
182
+ end
183
+ end
184
+
185
+ describe "extracts mentions" do
186
+ let(:issue_data) do
187
+ {
188
+ title: "Some title",
189
+ project: EventMock.new(title: "Project A"),
190
+ author: mock(title: "User A"),
191
+ watcher_users: [mock(title: "User C")],
192
+ status: "New",
193
+ priority: "Normal",
194
+ assigned_to: mock(title: "User B"),
195
+ description: "Some description @foo @NOT @bar"
196
+ }
197
+ end
198
+
199
+ it "should extract mentions for markdown" do
200
+ settings.text_formatting = "markdown"
201
+ msg[:text].must_include("To: @foo, @bar")
202
+ end
203
+
204
+ it "should NOT extract mentions for textile" do
205
+ settings.text_formatting = "textile"
206
+ msg[:text].wont_include("To: @foo, @bar")
207
+ end
208
+ end
209
+ end