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.
- checksums.yaml +4 -4
- data/.travis.yml +15 -0
- data/LICENSE +1 -0
- data/README.md +30 -17
- data/Rakefile +9 -0
- data/app/views/settings/_mattermost_settings.html.erb +60 -20
- data/lib/redmine-mattermost.rb +9 -1
- data/lib/redmine_mattermost/bridge.rb +63 -0
- data/lib/redmine_mattermost/client.rb +28 -0
- data/lib/redmine_mattermost/extractors.rb +88 -0
- data/lib/redmine_mattermost/extractors/applied_changeset.rb +55 -0
- data/lib/redmine_mattermost/extractors/changed_wiki_page.rb +39 -0
- data/lib/redmine_mattermost/extractors/new_issue.rb +44 -0
- data/lib/redmine_mattermost/extractors/updated_issue.rb +42 -0
- data/lib/redmine_mattermost/infos.rb +1 -2
- data/lib/redmine_mattermost/issue_patch.rb +14 -14
- data/lib/redmine_mattermost/listener.rb +38 -268
- data/lib/redmine_mattermost/message_builder.rb +61 -0
- data/lib/redmine_mattermost/redmine_plugin.rb +3 -4
- data/lib/redmine_mattermost/utils.rb +3 -0
- data/lib/redmine_mattermost/utils/journal_mapper.rb +75 -0
- data/lib/redmine_mattermost/utils/text.rb +37 -0
- data/lib/redmine_mattermost/utils/urls.rb +20 -0
- data/lib/redmine_mattermost/version.rb +1 -1
- data/redmine-mattermost.gemspec +4 -1
- data/spec/client_spec.rb +29 -0
- data/spec/extractors/applied_changeset_spec.rb +82 -0
- data/spec/extractors/changed_wiki_page_spec.rb +63 -0
- data/spec/extractors/new_issue_spec.rb +209 -0
- data/spec/extractors/updated_issue_spec.rb +169 -0
- data/spec/message_builder_spec.rb +71 -0
- data/spec/redmine_mattermost_spec.rb +9 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/mock_support.rb +113 -0
- data/spec/utils/text_spec.rb +142 -0
- data/spec/utils/text_spec_alt.rb +142 -0
- metadata +87 -9
@@ -0,0 +1,169 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe RedmineMattermost::Extractors::UpdatedIssue do
|
4
|
+
subject { RedmineMattermost::Extractors::UpdatedIssue.new(bridge) }
|
5
|
+
let(:result) { subject.from_context(context) }
|
6
|
+
let(:msg) { result[:message] }
|
7
|
+
let(:context) do
|
8
|
+
{ issue: issue, journal: journal }
|
9
|
+
end
|
10
|
+
let(:bridge) { MockBridge.new(settings) }
|
11
|
+
let(:issue) { MockIssue.new(issue_data) }
|
12
|
+
let(:journal) { mock(journal_data) }
|
13
|
+
|
14
|
+
let(:settings) { Defaults.settings }
|
15
|
+
let(:issue_data) do
|
16
|
+
{
|
17
|
+
title: "Some title",
|
18
|
+
project: EventMock.new(title: "Project A")
|
19
|
+
}
|
20
|
+
end
|
21
|
+
let(:journal_data) { Hash.new }
|
22
|
+
|
23
|
+
describe "empty context" do
|
24
|
+
let(:context) { Hash.new }
|
25
|
+
|
26
|
+
it "raises" do
|
27
|
+
proc { result }.must_raise KeyError
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "no project" do
|
32
|
+
let(:issue_data) do
|
33
|
+
{
|
34
|
+
title: "Some title"
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
it "returns nil" do
|
39
|
+
result.must_be_nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "minimal journal" do
|
44
|
+
let(:journal_data) do
|
45
|
+
{
|
46
|
+
details: [],
|
47
|
+
user: mock(title: "User A")
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should return a message" do
|
52
|
+
msg[:text].must_equal(
|
53
|
+
"[#{link_for("Project A")}] User A updated #{link_for("Some title")}"
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "full journal" do
|
59
|
+
let(:journal_data) do
|
60
|
+
{
|
61
|
+
details: [
|
62
|
+
mock(prop_key: "title", value: "Some title"),
|
63
|
+
mock(prop_key: "subject", value: "Some subject"),
|
64
|
+
mock(prop_key: "description", value: "Some description"),
|
65
|
+
mock(prop_key: "tracker_id", value: 1),
|
66
|
+
mock(prop_key: "project_id", value: 1),
|
67
|
+
mock(prop_key: "status_id", value: 1),
|
68
|
+
mock(prop_key: "priority_id", value: 1),
|
69
|
+
mock(prop_key: "category_id", value: 1),
|
70
|
+
mock(prop_key: "assigned_to", value: 1),
|
71
|
+
mock(prop_key: "fixed_version", value: 1),
|
72
|
+
mock(property: "attachment", prop_key: 1),
|
73
|
+
mock(prop_key: "parent_id", value: 1),
|
74
|
+
mock(prop_key: "other", value: "Some other"),
|
75
|
+
mock(property: "cf", prop_key: "empty_cf"),
|
76
|
+
mock(property: "cf", prop_key: "other_cf", value: "Some value")
|
77
|
+
],
|
78
|
+
user: mock(title: "User A"),
|
79
|
+
notes: "This is a @note@"
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should return a message" do
|
84
|
+
msg[:text].must_equal(
|
85
|
+
"[#{link_for("Project A")}] User A updated #{link_for("Some title")}"
|
86
|
+
)
|
87
|
+
|
88
|
+
attachments = msg[:attachments]
|
89
|
+
attachments.size.must_equal 1
|
90
|
+
attachment = attachments.shift
|
91
|
+
text = attachment[:text]
|
92
|
+
text.must_equal "This is a `note`"
|
93
|
+
fields = attachment[:fields]
|
94
|
+
fields.size.must_equal 15
|
95
|
+
fields.shift.must_equal({
|
96
|
+
title: "field_title",
|
97
|
+
value: "Some title"
|
98
|
+
})
|
99
|
+
fields.shift.must_equal({
|
100
|
+
title: "field_subject",
|
101
|
+
value: "Some subject"
|
102
|
+
})
|
103
|
+
fields.shift.must_equal({
|
104
|
+
title: "field_description",
|
105
|
+
value: "Some description"
|
106
|
+
})
|
107
|
+
fields.shift.must_equal({
|
108
|
+
title: "field_tracker",
|
109
|
+
value: "Tracker: 1",
|
110
|
+
short: true
|
111
|
+
})
|
112
|
+
fields.shift.must_equal({
|
113
|
+
title: "field_project",
|
114
|
+
value: "Project: 1",
|
115
|
+
short: true
|
116
|
+
})
|
117
|
+
fields.shift.must_equal({
|
118
|
+
title: "field_status",
|
119
|
+
value: "IssueStatus: 1",
|
120
|
+
short: true
|
121
|
+
})
|
122
|
+
fields.shift.must_equal({
|
123
|
+
title: "field_priority",
|
124
|
+
value: "IssuePriority: 1",
|
125
|
+
short: true
|
126
|
+
})
|
127
|
+
fields.shift.must_equal({
|
128
|
+
title: "field_category",
|
129
|
+
value: "IssueCategory: 1",
|
130
|
+
short: true
|
131
|
+
})
|
132
|
+
fields.shift.must_equal({
|
133
|
+
title: "field_assigned_to",
|
134
|
+
value: "User: 1",
|
135
|
+
short: true
|
136
|
+
})
|
137
|
+
fields.shift.must_equal({
|
138
|
+
title: "field_fixed_version",
|
139
|
+
value: "Version: 1",
|
140
|
+
short: true
|
141
|
+
})
|
142
|
+
fields.shift.must_equal({
|
143
|
+
title: "label_attachment",
|
144
|
+
value: link_for("file_1.png"),
|
145
|
+
short: true
|
146
|
+
})
|
147
|
+
fields.shift.must_equal({
|
148
|
+
title: "field_parent",
|
149
|
+
value: link_for("Issue: 1"),
|
150
|
+
short: true
|
151
|
+
})
|
152
|
+
fields.shift.must_equal({
|
153
|
+
title: "field_other",
|
154
|
+
value: "Some other",
|
155
|
+
short: true
|
156
|
+
})
|
157
|
+
fields.shift.must_equal({
|
158
|
+
title: "CustomField empty_cf",
|
159
|
+
value: "-",
|
160
|
+
short: true
|
161
|
+
})
|
162
|
+
fields.shift.must_equal({
|
163
|
+
title: "CustomField other_cf",
|
164
|
+
value: "Some value",
|
165
|
+
short: true
|
166
|
+
})
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe RedmineMattermost::MessageBuilder do
|
4
|
+
subject { RedmineMattermost::MessageBuilder.new(text) }
|
5
|
+
let(:text) { "Some message" }
|
6
|
+
let(:expected) do
|
7
|
+
{
|
8
|
+
link_names: 1,
|
9
|
+
text: text
|
10
|
+
}
|
11
|
+
end
|
12
|
+
it "returns the text as default" do
|
13
|
+
result.must_equal(expected)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "sets the username" do
|
17
|
+
subject.username("TestUser")
|
18
|
+
result.must_equal(expected.merge({
|
19
|
+
username: "TestUser"
|
20
|
+
}))
|
21
|
+
end
|
22
|
+
|
23
|
+
it "sets the channel" do
|
24
|
+
subject.channel("the-channel")
|
25
|
+
result.must_equal(expected.merge({
|
26
|
+
channel: "the-channel"
|
27
|
+
}))
|
28
|
+
end
|
29
|
+
|
30
|
+
it "adds attachments" do
|
31
|
+
subject.attachment.text("the text")
|
32
|
+
subject.attachment.text("other text")
|
33
|
+
.field("title", "value")
|
34
|
+
.field("foo", "bar", true)
|
35
|
+
|
36
|
+
result.must_equal(expected.merge({
|
37
|
+
attachments: [
|
38
|
+
{
|
39
|
+
text: "the text"
|
40
|
+
},
|
41
|
+
{
|
42
|
+
text: "other text",
|
43
|
+
fields: [
|
44
|
+
{
|
45
|
+
title: "title",
|
46
|
+
value: "value"
|
47
|
+
},
|
48
|
+
{
|
49
|
+
title: "foo",
|
50
|
+
value: "bar",
|
51
|
+
short: true
|
52
|
+
}
|
53
|
+
]
|
54
|
+
}
|
55
|
+
]
|
56
|
+
}))
|
57
|
+
end
|
58
|
+
|
59
|
+
it "set the icon as url" do
|
60
|
+
subject.icon("http://some.com/test.png")
|
61
|
+
result.must_equal(expected.merge({
|
62
|
+
icon_url: "http://some.com/test.png"
|
63
|
+
}))
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def result
|
69
|
+
subject.to_hash
|
70
|
+
end
|
71
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "redmine-mattermost"
|
2
|
+
|
3
|
+
require "minitest/autorun"
|
4
|
+
require "webmock/minitest"
|
5
|
+
|
6
|
+
require_relative "support/mock_support"
|
7
|
+
|
8
|
+
class Minitest::Test
|
9
|
+
def mock(data)
|
10
|
+
Mock.new(data)
|
11
|
+
end
|
12
|
+
|
13
|
+
def link_for(label)
|
14
|
+
'<URL: {"host":"test.host","protocol":"http","port":null,' \
|
15
|
+
'"script_name":null}|' + label + '>'
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
require "redcloth"
|
3
|
+
|
4
|
+
class Mock < OpenStruct
|
5
|
+
def to_s
|
6
|
+
title
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class EventMock < Mock
|
11
|
+
def event_url(options)
|
12
|
+
"URL: #{options.to_json}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class MockIssue < EventMock
|
17
|
+
def is_private?
|
18
|
+
is_private
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid?
|
22
|
+
valid
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class MockBridge
|
27
|
+
attr_reader :settings
|
28
|
+
|
29
|
+
def initialize(settings)
|
30
|
+
@settings = settings
|
31
|
+
end
|
32
|
+
|
33
|
+
def custom_field_by_name(name)
|
34
|
+
OpenStruct.new(name: name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def url_for(target)
|
38
|
+
target
|
39
|
+
end
|
40
|
+
|
41
|
+
def translate(key, options)
|
42
|
+
options[:locale].must_equal(settings.default_language)
|
43
|
+
key
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_html(formatting, text)
|
47
|
+
RedCloth.new(text).to_html
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_custom_field(id)
|
51
|
+
Mock.new(name: "CustomField #{id}")
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_tracker(id)
|
55
|
+
"Tracker: #{id}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_project(id)
|
59
|
+
"Project: #{id}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_issue_status(id)
|
63
|
+
"IssueStatus: #{id}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_issue_priority(id)
|
67
|
+
"IssuePriority: #{id}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_issue_category(id)
|
71
|
+
"IssueCategory: #{id}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_user(id)
|
75
|
+
"User: #{id}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_version(id)
|
79
|
+
"Version: #{id}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def find_attachement(id)
|
83
|
+
MockIssue.new(title: "Attachment: #{id}", filename: "file_#{id}.png")
|
84
|
+
end
|
85
|
+
|
86
|
+
def find_issue(id)
|
87
|
+
MockIssue.new(title: "Issue: #{id}")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Defaults
|
92
|
+
def self.settings
|
93
|
+
Mock.new({
|
94
|
+
host_name: "https://test.host",
|
95
|
+
protocol: "http",
|
96
|
+
port: 80,
|
97
|
+
script_name: "some_script",
|
98
|
+
plugin_redmine_mattermost: plugin_settings,
|
99
|
+
text_formatting: "textile",
|
100
|
+
default_language: "en"
|
101
|
+
})
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.plugin_settings
|
105
|
+
{
|
106
|
+
display_watchers: "1",
|
107
|
+
post_updates: "1",
|
108
|
+
post_wiki_updates: "1",
|
109
|
+
mattermost_url: "http://api.mattermost.org",
|
110
|
+
channel: "fallback-channel"
|
111
|
+
}
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe RedmineMattermost::Utils::Text do
|
4
|
+
subject { Dummy.new(bridge) }
|
5
|
+
let(:bridge) { MockBridge.new(settings) }
|
6
|
+
let(:settings) { Defaults.settings }
|
7
|
+
|
8
|
+
describe "h" do
|
9
|
+
it "escapes special chars" do
|
10
|
+
text = subject.h("some & special <chars>")
|
11
|
+
text.must_equal("some & special <chars>")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "handles nil inputs" do
|
15
|
+
subject.h(nil).must_be_nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "link" do
|
20
|
+
it "builds a markdown link" do
|
21
|
+
text = subject.link("the < label", "the_target")
|
22
|
+
text.must_equal("<the_target|the < label>")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "to_markdown" do
|
27
|
+
it "transforms HTML" do
|
28
|
+
input = <<HTML.strip
|
29
|
+
<h1>Header1</h1>
|
30
|
+
<h2>Header2</h2>
|
31
|
+
<p>This is a new comment</p>
|
32
|
+
<ul>
|
33
|
+
<li>with multiple items</li>
|
34
|
+
<li>in a list</li>
|
35
|
+
</ul>
|
36
|
+
|
37
|
+
<p>or a</p>
|
38
|
+
|
39
|
+
<ol>
|
40
|
+
<li>Numbered</li>
|
41
|
+
<li>List</li>
|
42
|
+
</ol>
|
43
|
+
|
44
|
+
<p>
|
45
|
+
<del>strike</del> <ins>underline</ins> <em>italic</em> <strong>bold</strong>
|
46
|
+
</p>
|
47
|
+
|
48
|
+
<blockquote>
|
49
|
+
<p>Block quote</p>
|
50
|
+
</blockquote>
|
51
|
+
|
52
|
+
<p>
|
53
|
+
<img src="https://www.example.org/image.png" alt="Example Image">
|
54
|
+
</p>
|
55
|
+
|
56
|
+
<p>
|
57
|
+
External link: <a href="https://www.example.org" class="external">ExampleORG</a>
|
58
|
+
</p>
|
59
|
+
|
60
|
+
<pre>
|
61
|
+
Some pre-indented text
|
62
|
+
in two lines
|
63
|
+
</pre>
|
64
|
+
|
65
|
+
<pre><code class="ruby syntaxhl">
|
66
|
+
class Ruby
|
67
|
+
end
|
68
|
+
</code></pre>
|
69
|
+
|
70
|
+
HTML
|
71
|
+
expected = <<MARKDOWN.strip
|
72
|
+
# Header1
|
73
|
+
|
74
|
+
## Header2
|
75
|
+
|
76
|
+
This is a new comment
|
77
|
+
|
78
|
+
- with multiple items
|
79
|
+
- in a list
|
80
|
+
|
81
|
+
or a
|
82
|
+
|
83
|
+
1. Numbered
|
84
|
+
2. List
|
85
|
+
|
86
|
+
~~strike~~ underline _italic_ **bold**
|
87
|
+
|
88
|
+
> Block quote
|
89
|
+
|
90
|
+

|
91
|
+
|
92
|
+
External link: [ExampleORG](https://www.example.org)
|
93
|
+
|
94
|
+
```
|
95
|
+
Some pre-indented text
|
96
|
+
in two lines
|
97
|
+
```
|
98
|
+
|
99
|
+
```
|
100
|
+
class Ruby
|
101
|
+
end
|
102
|
+
```
|
103
|
+
MARKDOWN
|
104
|
+
text = subject.to_markdown(input)
|
105
|
+
text.must_equal(expected)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "passes already used markdown" do
|
109
|
+
settings.text_formatting = "markdown"
|
110
|
+
text = subject.to_markdown("> Markdown")
|
111
|
+
text.must_equal("> Markdown")
|
112
|
+
end
|
113
|
+
|
114
|
+
it "rescues to escaped test" do
|
115
|
+
stub = proc do |formatting, text|
|
116
|
+
raise "some error"
|
117
|
+
end
|
118
|
+
bridge.stub(:to_html, stub) do
|
119
|
+
text = subject.to_markdown("<code>")
|
120
|
+
text.must_equal("<code>")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
class Dummy
|
128
|
+
include RedmineMattermost::Utils::Text
|
129
|
+
|
130
|
+
attr_reader :bridge
|
131
|
+
|
132
|
+
def initialize(bridge)
|
133
|
+
@bridge = bridge
|
134
|
+
end
|
135
|
+
|
136
|
+
# just use the input (assuming the test already)
|
137
|
+
# provides valid HTML
|
138
|
+
def to_html(formatting, text)
|
139
|
+
text
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|