Skip to content

Commit 4ef189d

Browse files
Fix class loader issue for notifications response (#40)
* Fix class loader issue for notifications Signed-off-by: Joshua Li <joshuali925@gmail.com> * Fix formatting Signed-off-by: Joshua Li <joshuali925@gmail.com> * Refactor creation of action listener object Signed-off-by: Joshua Li <joshuali925@gmail.com> * Fix indentation Signed-off-by: Joshua Li <joshuali925@gmail.com> * Remove unused suppresses Signed-off-by: Joshua Li <joshuali925@gmail.com> * Add UT for notification API Signed-off-by: Chen Dai <daichen@amazon.com> * Add UT for notification API Signed-off-by: Chen Dai <daichen@amazon.com> * Add UT for send notification API Signed-off-by: Chen Dai <daichen@amazon.com> * Fix Github workflow failure Signed-off-by: Chen Dai <daichen@amazon.com> * Fix Github workflow failure Signed-off-by: Chen Dai <daichen@amazon.com> * Refactor UT code Signed-off-by: Chen Dai <daichen@amazon.com> Co-authored-by: Joshua Li <joshuali925@gmail.com>
1 parent 62de872 commit 4ef189d

File tree

3 files changed

+304
-8
lines changed

3 files changed

+304
-8
lines changed

src/main/kotlin/org/opensearch/commons/notifications/NotificationsPluginInterface.kt

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@
2727
package org.opensearch.commons.notifications
2828

2929
import org.opensearch.action.ActionListener
30+
import org.opensearch.action.ActionResponse
3031
import org.opensearch.client.node.NodeClient
32+
import org.opensearch.common.io.stream.Writeable
3133
import org.opensearch.commons.ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT
34+
import org.opensearch.commons.notifications.action.BaseResponse
3235
import org.opensearch.commons.notifications.action.CreateNotificationConfigRequest
3336
import org.opensearch.commons.notifications.action.CreateNotificationConfigResponse
3437
import org.opensearch.commons.notifications.action.DeleteNotificationConfigRequest
@@ -56,6 +59,7 @@ import org.opensearch.commons.notifications.action.UpdateNotificationConfigRespo
5659
import org.opensearch.commons.notifications.model.ChannelMessage
5760
import org.opensearch.commons.notifications.model.EventSource
5861
import org.opensearch.commons.utils.SecureClientWrapper
62+
import org.opensearch.commons.utils.recreateObject
5963

6064
/**
6165
* All the transport action plugin interfaces for the Notification plugin
@@ -76,7 +80,7 @@ object NotificationsPluginInterface {
7680
client.execute(
7781
CREATE_NOTIFICATION_CONFIG_ACTION_TYPE,
7882
request,
79-
listener
83+
wrapActionListener(listener) { response -> recreateObject(response) { CreateNotificationConfigResponse(it) } }
8084
)
8185
}
8286

@@ -94,7 +98,7 @@ object NotificationsPluginInterface {
9498
client.execute(
9599
UPDATE_NOTIFICATION_CONFIG_ACTION_TYPE,
96100
request,
97-
listener
101+
wrapActionListener(listener) { response -> recreateObject(response) { UpdateNotificationConfigResponse(it) } }
98102
)
99103
}
100104

@@ -112,7 +116,7 @@ object NotificationsPluginInterface {
112116
client.execute(
113117
DELETE_NOTIFICATION_CONFIG_ACTION_TYPE,
114118
request,
115-
listener
119+
wrapActionListener(listener) { response -> recreateObject(response) { DeleteNotificationConfigResponse(it) } }
116120
)
117121
}
118122

@@ -130,7 +134,7 @@ object NotificationsPluginInterface {
130134
client.execute(
131135
GET_NOTIFICATION_CONFIG_ACTION_TYPE,
132136
request,
133-
listener
137+
wrapActionListener(listener) { response -> recreateObject(response) { GetNotificationConfigResponse(it) } }
134138
)
135139
}
136140

@@ -148,7 +152,7 @@ object NotificationsPluginInterface {
148152
client.execute(
149153
GET_NOTIFICATION_EVENT_ACTION_TYPE,
150154
request,
151-
listener
155+
wrapActionListener(listener) { response -> recreateObject(response) { GetNotificationEventResponse(it) } }
152156
)
153157
}
154158

@@ -166,7 +170,7 @@ object NotificationsPluginInterface {
166170
client.execute(
167171
GET_PLUGIN_FEATURES_ACTION_TYPE,
168172
request,
169-
listener
173+
wrapActionListener(listener) { response -> recreateObject(response) { GetPluginFeaturesResponse(it) } }
170174
)
171175
}
172176

@@ -184,7 +188,7 @@ object NotificationsPluginInterface {
184188
client.execute(
185189
GET_FEATURE_CHANNEL_LIST_ACTION_TYPE,
186190
request,
187-
listener
191+
wrapActionListener(listener) { response -> recreateObject(response) { GetFeatureChannelListResponse(it) } }
188192
)
189193
}
190194

@@ -209,7 +213,30 @@ object NotificationsPluginInterface {
209213
wrapper.execute(
210214
SEND_NOTIFICATION_ACTION_TYPE,
211215
SendNotificationRequest(eventSource, channelMessage, channelIds, threadContext),
212-
listener
216+
wrapActionListener(listener) { response -> recreateObject(response) { SendNotificationResponse(it) } }
213217
)
214218
}
219+
220+
/**
221+
* Wrap action listener on concrete response class by a new created one on ActionResponse.
222+
* This is required because the response may be loaded by different classloader across plugins.
223+
* The onResponse(ActionResponse) avoids type cast exception and give a chance to recreate
224+
* the response object.
225+
*/
226+
@Suppress("UNCHECKED_CAST")
227+
private fun <Response : BaseResponse> wrapActionListener(
228+
listener: ActionListener<Response>,
229+
recreate: (Writeable) -> Response
230+
): ActionListener<Response> {
231+
return object : ActionListener<ActionResponse> {
232+
override fun onResponse(response: ActionResponse) {
233+
val recreated = response as? Response ?: recreate(response)
234+
listener.onResponse(recreated)
235+
}
236+
237+
override fun onFailure(exception: java.lang.Exception) {
238+
listener.onFailure(exception)
239+
}
240+
} as ActionListener<Response>
241+
}
215242
}
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*
8+
* Modifications Copyright OpenSearch Contributors. See
9+
* GitHub history for details.
10+
*/
11+
package org.opensearch.commons.notifications
12+
13+
import com.nhaarman.mockitokotlin2.whenever
14+
import org.junit.jupiter.api.Test
15+
import org.junit.jupiter.api.extension.ExtendWith
16+
import org.mockito.Answers
17+
import org.mockito.ArgumentMatchers.eq
18+
import org.mockito.Mock
19+
import org.mockito.Mockito.any
20+
import org.mockito.Mockito.doAnswer
21+
import org.mockito.Mockito.mock
22+
import org.mockito.Mockito.times
23+
import org.mockito.Mockito.verify
24+
import org.mockito.junit.jupiter.MockitoExtension
25+
import org.opensearch.action.ActionListener
26+
import org.opensearch.action.ActionType
27+
import org.opensearch.client.node.NodeClient
28+
import org.opensearch.commons.notifications.action.CreateNotificationConfigRequest
29+
import org.opensearch.commons.notifications.action.CreateNotificationConfigResponse
30+
import org.opensearch.commons.notifications.action.DeleteNotificationConfigRequest
31+
import org.opensearch.commons.notifications.action.DeleteNotificationConfigResponse
32+
import org.opensearch.commons.notifications.action.GetFeatureChannelListRequest
33+
import org.opensearch.commons.notifications.action.GetFeatureChannelListResponse
34+
import org.opensearch.commons.notifications.action.GetNotificationConfigRequest
35+
import org.opensearch.commons.notifications.action.GetNotificationConfigResponse
36+
import org.opensearch.commons.notifications.action.GetNotificationEventRequest
37+
import org.opensearch.commons.notifications.action.GetNotificationEventResponse
38+
import org.opensearch.commons.notifications.action.GetPluginFeaturesRequest
39+
import org.opensearch.commons.notifications.action.GetPluginFeaturesResponse
40+
import org.opensearch.commons.notifications.action.SendNotificationResponse
41+
import org.opensearch.commons.notifications.action.UpdateNotificationConfigRequest
42+
import org.opensearch.commons.notifications.action.UpdateNotificationConfigResponse
43+
import org.opensearch.commons.notifications.model.ChannelMessage
44+
import org.opensearch.commons.notifications.model.ConfigType
45+
import org.opensearch.commons.notifications.model.DeliveryStatus
46+
import org.opensearch.commons.notifications.model.EventSource
47+
import org.opensearch.commons.notifications.model.EventStatus
48+
import org.opensearch.commons.notifications.model.Feature
49+
import org.opensearch.commons.notifications.model.FeatureChannel
50+
import org.opensearch.commons.notifications.model.FeatureChannelList
51+
import org.opensearch.commons.notifications.model.NotificationConfig
52+
import org.opensearch.commons.notifications.model.NotificationConfigInfo
53+
import org.opensearch.commons.notifications.model.NotificationConfigSearchResult
54+
import org.opensearch.commons.notifications.model.NotificationEvent
55+
import org.opensearch.commons.notifications.model.NotificationEventInfo
56+
import org.opensearch.commons.notifications.model.NotificationEventSearchResult
57+
import org.opensearch.commons.notifications.model.SeverityType
58+
import org.opensearch.commons.notifications.model.Slack
59+
import org.opensearch.rest.RestStatus
60+
import java.time.Instant
61+
import java.util.EnumSet
62+
63+
@Suppress("UNCHECKED_CAST")
64+
@ExtendWith(MockitoExtension::class)
65+
internal class NotificationsPluginInterfaceTests {
66+
67+
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
68+
private lateinit var client: NodeClient
69+
70+
@Test
71+
fun createNotificationConfig() {
72+
val request = mock(CreateNotificationConfigRequest::class.java)
73+
val response = CreateNotificationConfigResponse("configId")
74+
val listener: ActionListener<CreateNotificationConfigResponse> =
75+
mock(ActionListener::class.java) as ActionListener<CreateNotificationConfigResponse>
76+
77+
doAnswer {
78+
(it.getArgument(2) as ActionListener<CreateNotificationConfigResponse>)
79+
.onResponse(response)
80+
}.whenever(client).execute(any(ActionType::class.java), any(), any())
81+
82+
NotificationsPluginInterface.createNotificationConfig(client, request, listener)
83+
verify(listener, times(1)).onResponse(eq(response))
84+
}
85+
86+
@Test
87+
fun updateNotificationConfig() {
88+
val request = mock(UpdateNotificationConfigRequest::class.java)
89+
val response = UpdateNotificationConfigResponse("configId")
90+
val listener: ActionListener<UpdateNotificationConfigResponse> =
91+
mock(ActionListener::class.java) as ActionListener<UpdateNotificationConfigResponse>
92+
93+
doAnswer {
94+
(it.getArgument(2) as ActionListener<UpdateNotificationConfigResponse>)
95+
.onResponse(response)
96+
}.whenever(client).execute(any(ActionType::class.java), any(), any())
97+
98+
NotificationsPluginInterface.updateNotificationConfig(client, request, listener)
99+
verify(listener, times(1)).onResponse(eq(response))
100+
}
101+
102+
@Test
103+
fun deleteNotificationConfig() {
104+
val request = mock(DeleteNotificationConfigRequest::class.java)
105+
val response = DeleteNotificationConfigResponse(mapOf(Pair("sample_config_id", RestStatus.OK)))
106+
val listener: ActionListener<DeleteNotificationConfigResponse> =
107+
mock(ActionListener::class.java) as ActionListener<DeleteNotificationConfigResponse>
108+
109+
doAnswer {
110+
(it.getArgument(2) as ActionListener<DeleteNotificationConfigResponse>)
111+
.onResponse(response)
112+
}.whenever(client).execute(any(ActionType::class.java), any(), any())
113+
114+
NotificationsPluginInterface.deleteNotificationConfig(client, request, listener)
115+
verify(listener, times(1)).onResponse(eq(response))
116+
}
117+
118+
@Test
119+
fun getNotificationConfig() {
120+
val request = mock(GetNotificationConfigRequest::class.java)
121+
val response = mockGetNotificationConfigResponse()
122+
val listener: ActionListener<GetNotificationConfigResponse> =
123+
mock(ActionListener::class.java) as ActionListener<GetNotificationConfigResponse>
124+
125+
doAnswer {
126+
(it.getArgument(2) as ActionListener<GetNotificationConfigResponse>)
127+
.onResponse(response)
128+
}.whenever(client).execute(any(ActionType::class.java), any(), any())
129+
130+
NotificationsPluginInterface.getNotificationConfig(client, request, listener)
131+
verify(listener, times(1)).onResponse(eq(response))
132+
}
133+
134+
@Test
135+
fun getNotificationEvent() {
136+
val request = mock(GetNotificationEventRequest::class.java)
137+
val response = mockGetNotificationEventResponse()
138+
val listener: ActionListener<GetNotificationEventResponse> =
139+
mock(ActionListener::class.java) as ActionListener<GetNotificationEventResponse>
140+
141+
doAnswer {
142+
(it.getArgument(2) as ActionListener<GetNotificationEventResponse>)
143+
.onResponse(response)
144+
}.whenever(client).execute(any(ActionType::class.java), any(), any())
145+
146+
NotificationsPluginInterface.getNotificationEvent(client, request, listener)
147+
verify(listener, times(1)).onResponse(eq(response))
148+
}
149+
150+
@Test
151+
fun getPluginFeatures() {
152+
val request = mock(GetPluginFeaturesRequest::class.java)
153+
val response = GetPluginFeaturesResponse(
154+
listOf("config_type_1", "config_type_2", "config_type_3"),
155+
mapOf(
156+
Pair("FeatureKey1", "FeatureValue1"),
157+
Pair("FeatureKey2", "FeatureValue2"),
158+
Pair("FeatureKey3", "FeatureValue3")
159+
)
160+
)
161+
val listener: ActionListener<GetPluginFeaturesResponse> =
162+
mock(ActionListener::class.java) as ActionListener<GetPluginFeaturesResponse>
163+
164+
doAnswer {
165+
(it.getArgument(2) as ActionListener<GetPluginFeaturesResponse>)
166+
.onResponse(response)
167+
}.whenever(client).execute(any(ActionType::class.java), any(), any())
168+
169+
NotificationsPluginInterface.getPluginFeatures(client, request, listener)
170+
verify(listener, times(1)).onResponse(eq(response))
171+
}
172+
173+
@Test
174+
fun getFeatureChannelList() {
175+
val sampleConfig = FeatureChannel(
176+
"config_id",
177+
"name",
178+
"description",
179+
ConfigType.SLACK
180+
)
181+
182+
val request = mock(GetFeatureChannelListRequest::class.java)
183+
val response = GetFeatureChannelListResponse(FeatureChannelList(sampleConfig))
184+
val listener: ActionListener<GetFeatureChannelListResponse> =
185+
mock(ActionListener::class.java) as ActionListener<GetFeatureChannelListResponse>
186+
187+
doAnswer {
188+
(it.getArgument(2) as ActionListener<GetFeatureChannelListResponse>)
189+
.onResponse(response)
190+
}.whenever(client).execute(any(ActionType::class.java), any(), any())
191+
192+
NotificationsPluginInterface.getFeatureChannelList(client, request, listener)
193+
verify(listener, times(1)).onResponse(eq(response))
194+
}
195+
196+
@Test
197+
fun sendNotification() {
198+
val notificationInfo = EventSource(
199+
"title",
200+
"reference_id",
201+
Feature.REPORTS,
202+
SeverityType.HIGH,
203+
listOf("tag1", "tag2")
204+
)
205+
val channelMessage = ChannelMessage(
206+
"text_description",
207+
"<b>htmlDescription</b>",
208+
null
209+
)
210+
211+
val response = SendNotificationResponse("configId")
212+
val listener: ActionListener<SendNotificationResponse> =
213+
mock(ActionListener::class.java) as ActionListener<SendNotificationResponse>
214+
215+
doAnswer {
216+
(it.getArgument(2) as ActionListener<SendNotificationResponse>)
217+
.onResponse(response)
218+
}.whenever(client).execute(any(ActionType::class.java), any(), any())
219+
220+
NotificationsPluginInterface.sendNotification(
221+
client, notificationInfo, channelMessage, listOf("channelId1", "channelId2"), listener
222+
)
223+
verify(listener, times(1)).onResponse(eq(response))
224+
}
225+
226+
private fun mockGetNotificationConfigResponse(): GetNotificationConfigResponse {
227+
val sampleSlack = Slack("https://domain.com/sample_url#1234567890")
228+
val sampleConfig = NotificationConfig(
229+
"name",
230+
"description",
231+
ConfigType.SLACK,
232+
EnumSet.of(Feature.REPORTS),
233+
configData = sampleSlack
234+
)
235+
val configInfo = NotificationConfigInfo(
236+
"config_id",
237+
Instant.now(),
238+
Instant.now(),
239+
"tenant",
240+
sampleConfig
241+
)
242+
return GetNotificationConfigResponse(NotificationConfigSearchResult(configInfo))
243+
}
244+
245+
private fun mockGetNotificationEventResponse(): GetNotificationEventResponse {
246+
val sampleEventSource = EventSource(
247+
"title",
248+
"reference_id",
249+
Feature.ALERTING,
250+
severity = SeverityType.INFO
251+
)
252+
val sampleStatus = EventStatus(
253+
"config_id",
254+
"name",
255+
ConfigType.SLACK,
256+
deliveryStatus = DeliveryStatus("404", "invalid recipient")
257+
)
258+
val sampleEvent = NotificationEvent(sampleEventSource, listOf(sampleStatus))
259+
val eventInfo = NotificationEventInfo(
260+
"event_id",
261+
Instant.now(),
262+
Instant.now(),
263+
"tenant",
264+
sampleEvent
265+
)
266+
return GetNotificationEventResponse(NotificationEventSearchResult(eventInfo))
267+
}
268+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mock-maker-inline

0 commit comments

Comments
 (0)