Skip to content

Commit 5765bf7

Browse files
authored
Merge pull request #554 from atlassian-labs/issue/master/DCPL-4351-fix-slack-notification-on-conf
Summary This PR fixes a bug where Confluence page and blog creation/update events never produced Slack notifications, and adds functional tests to verify the notification pipeline end-to-end. Changes Bug Fix • slack-server-integration-common/.../factory/SlackNotificationContextDescriptorFactory.java Changed descriptor class from SlackNotificationDescriptor → SlackNotificationContextDescriptor New Functional Tests • confluence-slack-integration/.../functional/PageNotificationFuncTest.java 8 end-to-end functional tests verifying chat.postMessage is called correctly for Confluence events:
2 parents a7c55d5 + 099291a commit 5765bf7

3 files changed

Lines changed: 242 additions & 4 deletions

File tree

confluence-slack-integration/confluence-slack-server-integration-plugin/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<confluence.amps.version>${amps.version}</confluence.amps.version>
2626

2727
<!-- Confluence version used for integration tests; can be overridden to test against different versions -->
28-
<confluence.version>10.0.1</confluence.version>
28+
<confluence.version>10.2.11</confluence.version>
2929
<confluence.data.version>${confluence.version}</confluence.data.version>
3030

3131
<!-- Confluence API version used during the build -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package it.com.atlassian.confluence.plugins.slack.functional;
2+
3+
import com.atlassian.plugins.slack.test.RequestMatchers;
4+
import com.github.seratch.jslack.api.methods.request.chat.ChatPostMessageRequest;
5+
import it.com.atlassian.confluence.plugins.slack.util.SlackFunctionalTestBase;
6+
import okhttp3.Credentials;
7+
import okhttp3.MediaType;
8+
import okhttp3.OkHttpClient;
9+
import okhttp3.Request;
10+
import okhttp3.RequestBody;
11+
import okhttp3.Response;
12+
import org.json.JSONObject;
13+
import org.junit.jupiter.api.BeforeEach;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.TestInfo;
16+
17+
import java.io.IOException;
18+
import java.lang.reflect.Method;
19+
20+
import static com.atlassian.plugins.slack.test.RequestMatchers.hasHit;
21+
import static com.atlassian.plugins.slack.test.TestChannels.PUBLIC;
22+
import static com.atlassian.plugins.slack.test.TestTeams.DUMMY_TEAM;
23+
import static com.github.seratch.jslack.api.methods.Methods.CHAT_POST_MESSAGE;
24+
import static org.hamcrest.MatcherAssert.assertThat;
25+
import static org.hamcrest.Matchers.allOf;
26+
import static org.hamcrest.Matchers.contains;
27+
import static org.hamcrest.Matchers.containsString;
28+
import static org.hamcrest.Matchers.is;
29+
30+
/**
31+
* Functional tests verifying that Confluence page and blog creation/update events
32+
* produce the correct {@code chat.postMessage} notifications to the Slack mock server.
33+
*/
34+
public class PageNotificationFuncTest extends SlackFunctionalTestBase {
35+
36+
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
37+
private static final String ADMIN_USER = "admin";
38+
private static final String ADMIN_PASS = "admin";
39+
40+
// Unique per test-run + per test-method to avoid Confluence "title already exists" 400s.
41+
// Confluence persists data across test runs so titles must never repeat.
42+
private static final String RUN_ID = String.valueOf(System.currentTimeMillis());
43+
private String PAGE_TITLE;
44+
private String BLOG_TITLE;
45+
46+
private OkHttpClient httpClient;
47+
private String confluenceBaseUrl;
48+
49+
@BeforeEach
50+
void setup(TestInfo testInfo) {
51+
// Unique title per run + per method — never conflicts with previous runs
52+
String suffix = RUN_ID + "-" + testInfo.getTestMethod().map(Method::getName).orElse("test");
53+
PAGE_TITLE = "Page Notification " + suffix;
54+
BLOG_TITLE = "Blog Notification " + suffix;
55+
56+
confluenceBaseUrl = client.instance().getBaseUrl();
57+
httpClient = new OkHttpClient.Builder()
58+
.addInterceptor(chain -> {
59+
Request authenticatedReq = chain.request().newBuilder()
60+
.header("Authorization", Credentials.basic(ADMIN_USER, ADMIN_PASS))
61+
.build();
62+
return chain.proceed(authenticatedReq);
63+
}).build();
64+
65+
connectToDummyTeamWithCustomInstall();
66+
createTestSpace();
67+
enableNotification("PageCreate");
68+
enableNotification("PageUpdate");
69+
enableNotification("BlogCreate");
70+
}
71+
72+
@Test
73+
void pageCreation_withNotificationEnabled_shouldPostNotification() {
74+
server.clearHistoryExecuteAndWaitForNewRequest(CHAT_POST_MESSAGE, () ->
75+
createPage(PAGE_TITLE));
76+
77+
assertNotificationSent("created", "page", PAGE_TITLE);
78+
}
79+
80+
@Test
81+
void pageCreation_notificationSentToCorrectChannel() {
82+
server.clearHistoryExecuteAndWaitForNewRequest(CHAT_POST_MESSAGE, () ->
83+
createPage(PAGE_TITLE));
84+
85+
assertThat(server.requestHistoryForTest(), hasHit(CHAT_POST_MESSAGE, contains(allOf(
86+
RequestMatchers.requestEntityProperty(ChatPostMessageRequest::getChannel,
87+
is(PUBLIC.getId()))
88+
))));
89+
}
90+
91+
@Test
92+
void pageCreation_withNotificationDisabled_shouldNotPostNotification() {
93+
disableNotification("PageCreate");
94+
95+
server.clearHistoryExecuteAndExpectNoRequests(CHAT_POST_MESSAGE, () ->
96+
createPage(PAGE_TITLE));
97+
}
98+
99+
@Test
100+
void pageUpdate_withNotificationEnabled_shouldPostNotification() {
101+
String pageId = createPageAndGetId(PAGE_TITLE + " for Update");
102+
103+
server.clearHistoryExecuteAndWaitForNewRequest(CHAT_POST_MESSAGE, () ->
104+
updatePage(pageId, PAGE_TITLE + " for Update", 2));
105+
106+
assertNotificationSent("updated", "page", PAGE_TITLE + " for Update");
107+
}
108+
109+
@Test
110+
void pageUpdate_withNotificationDisabled_shouldNotPostNotification() {
111+
disableNotification("PageUpdate");
112+
String pageId = createPageAndGetId(PAGE_TITLE + " Silent Update");
113+
114+
server.clearHistoryExecuteAndExpectNoRequests(CHAT_POST_MESSAGE, () ->
115+
updatePage(pageId, PAGE_TITLE + " Silent Update", 2));
116+
}
117+
118+
@Test
119+
void blogCreation_withNotificationEnabled_shouldPostNotification() {
120+
server.clearHistoryExecuteAndWaitForNewRequest(CHAT_POST_MESSAGE, () ->
121+
createBlogPost(BLOG_TITLE));
122+
123+
assertNotificationSent("created", "blog", BLOG_TITLE);
124+
}
125+
126+
@Test
127+
void blogCreation_notificationSentToCorrectChannel() {
128+
server.clearHistoryExecuteAndWaitForNewRequest(CHAT_POST_MESSAGE, () ->
129+
createBlogPost(BLOG_TITLE));
130+
131+
assertThat(server.requestHistoryForTest(), hasHit(CHAT_POST_MESSAGE, contains(allOf(
132+
RequestMatchers.requestEntityProperty(ChatPostMessageRequest::getChannel,
133+
is(PUBLIC.getId()))
134+
))));
135+
}
136+
137+
@Test
138+
void blogCreation_withNotificationDisabled_shouldNotPostNotification() {
139+
disableNotification("BlogCreate");
140+
141+
server.clearHistoryExecuteAndExpectNoRequests(CHAT_POST_MESSAGE, () ->
142+
createBlogPost(BLOG_TITLE));
143+
}
144+
145+
/**
146+
* Asserts that exactly one {@code chat.postMessage} was sent to the public channel,
147+
* with notification text containing the action verb, content type, and content title.
148+
*
149+
* @param action e.g. "created", "updated"
150+
* @param contentType e.g. "page", "blog"
151+
* @param title the page or blog title
152+
*/
153+
private void assertNotificationSent(String action, String contentType, String title) {
154+
assertThat(server.requestHistoryForTest(), hasHit(CHAT_POST_MESSAGE, contains(allOf(
155+
RequestMatchers.requestEntityProperty(ChatPostMessageRequest::getText, allOf(
156+
containsString("*" + action + "*"),
157+
containsString(contentType),
158+
containsString(title))),
159+
RequestMatchers.requestEntityProperty(ChatPostMessageRequest::getChannel,
160+
is(PUBLIC.getId()))
161+
))));
162+
}
163+
164+
private void enableNotification(String notificationKey) {
165+
client.admin().notifications().enable(SPACE_KEY, DUMMY_TEAM.getTeamId(), PUBLIC.getId(), notificationKey, false);
166+
}
167+
168+
private void disableNotification(String notificationKey) {
169+
client.admin().notifications().disable(SPACE_KEY, DUMMY_TEAM.getTeamId(), PUBLIC.getId(), notificationKey);
170+
}
171+
172+
private void createPage(String title) {
173+
executePost(contentPayload("page", title, "<p>Test content for " + title + "</p>"));
174+
}
175+
176+
private String createPageAndGetId(String title) {
177+
String responseBody = executePostAndReturnBody(
178+
contentPayload("page", title, "<p>Test content for " + title + "</p>"));
179+
try {
180+
return new JSONObject(responseBody).getString("id");
181+
} catch (Exception e) {
182+
throw new RuntimeException("Could not parse page ID from: " + responseBody, e);
183+
}
184+
}
185+
186+
private void updatePage(String pageId, String title, int version) {
187+
String json = "{"
188+
+ "\"version\":{\"number\":" + version + "},"
189+
+ "\"type\":\"page\","
190+
+ "\"title\":\"" + title + "\","
191+
+ "\"body\":{\"storage\":{"
192+
+ "\"value\":\"<p>Updated content</p>\","
193+
+ "\"representation\":\"storage\"}}"
194+
+ "}";
195+
String url = confluenceBaseUrl + "/rest/api/content/" + pageId;
196+
try (Response response = httpClient.newCall(
197+
new Request.Builder().url(url).put(RequestBody.create(json, JSON)).build()
198+
).execute()) {
199+
if (!response.isSuccessful()) {
200+
throw new RuntimeException("Page update failed: HTTP " + response.code());
201+
}
202+
} catch (IOException e) {
203+
throw new RuntimeException("Page update request failed", e);
204+
}
205+
}
206+
207+
private void createBlogPost(String title) {
208+
executePost(contentPayload("blogpost", title, "<p>Blog content for " + title + "</p>"));
209+
}
210+
211+
private void executePost(String json) {
212+
executePostAndReturnBody(json);
213+
}
214+
215+
private String executePostAndReturnBody(String json) {
216+
String url = confluenceBaseUrl + "/rest/api/content";
217+
try (Response response = httpClient.newCall(
218+
new Request.Builder().url(url).post(RequestBody.create(json, JSON)).build()
219+
).execute()) {
220+
String body = response.body() != null ? response.body().string() : "";
221+
if (!response.isSuccessful()) {
222+
throw new RuntimeException("Content creation failed: HTTP " + response.code() + " — " + body);
223+
}
224+
return body;
225+
} catch (IOException e) {
226+
throw new RuntimeException("Content creation request failed", e);
227+
}
228+
}
229+
230+
private String contentPayload(String type, String title, String htmlContent) {
231+
return "{\"type\":\"" + type + "\","
232+
+ "\"title\":\"" + title + "\","
233+
+ "\"space\":{\"key\":\"" + SPACE_KEY + "\"},"
234+
+ "\"body\":{\"storage\":{"
235+
+ "\"value\":\"" + htmlContent + "\","
236+
+ "\"representation\":\"storage\"}}}";
237+
}
238+
}

slack-server-integration-common/src/main/java/com/atlassian/plugins/slack/api/descriptor/factory/SlackNotificationContextDescriptorFactory.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
import com.atlassian.plugin.osgi.external.ListableModuleDescriptorFactory;
55
import com.atlassian.plugin.osgi.external.SingleModuleDescriptorFactory;
66
import com.atlassian.plugin.spring.scanner.annotation.export.ModuleType;
7-
import com.atlassian.plugins.slack.api.descriptor.SlackNotificationDescriptor;
7+
import com.atlassian.plugins.slack.api.descriptor.SlackNotificationContextDescriptor;
88
import org.springframework.beans.factory.annotation.Autowired;
99
import org.springframework.stereotype.Component;
1010

1111
@Component
1212
@ModuleType(ListableModuleDescriptorFactory.class)
13-
public class SlackNotificationContextDescriptorFactory extends SingleModuleDescriptorFactory<SlackNotificationDescriptor> {
13+
public class SlackNotificationContextDescriptorFactory extends SingleModuleDescriptorFactory<SlackNotificationContextDescriptor> {
1414
@Autowired
1515
public SlackNotificationContextDescriptorFactory(final HostContainer hostContainer) {
16-
super(hostContainer, "slack-notification-context", SlackNotificationDescriptor.class);
16+
super(hostContainer, "slack-notification-context", SlackNotificationContextDescriptor.class);
1717
}
1818
}
1919

0 commit comments

Comments
 (0)