redmine-mattermost 0.3 → 0.4.beta1

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 (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