|
| 1 | +package test_with_remote_apis; |
| 2 | + |
| 3 | +import com.slack.api.Slack; |
| 4 | +import com.slack.api.methods.AsyncMethodsClient; |
| 5 | +import com.slack.api.methods.MethodsClient; |
| 6 | +import com.slack.api.methods.SlackApiException; |
| 7 | +import com.slack.api.methods.request.canvases.sections.CanvasesSectionsLookupRequest; |
| 8 | +import com.slack.api.methods.response.bookmarks.BookmarksAddResponse; |
| 9 | +import com.slack.api.methods.response.bookmarks.BookmarksEditResponse; |
| 10 | +import com.slack.api.methods.response.bookmarks.BookmarksListResponse; |
| 11 | +import com.slack.api.methods.response.bookmarks.BookmarksRemoveResponse; |
| 12 | +import com.slack.api.methods.response.bots.BotsInfoResponse; |
| 13 | +import com.slack.api.methods.response.canvases.CanvasesCreateResponse; |
| 14 | +import com.slack.api.methods.response.canvases.CanvasesDeleteResponse; |
| 15 | +import com.slack.api.methods.response.canvases.CanvasesEditResponse; |
| 16 | +import com.slack.api.methods.response.canvases.access.CanvasesAccessDeleteResponse; |
| 17 | +import com.slack.api.methods.response.canvases.access.CanvasesAccessSetResponse; |
| 18 | +import com.slack.api.methods.response.canvases.sections.CanvasesSectionsLookupResponse; |
| 19 | +import com.slack.api.methods.response.chat.ChatDeleteResponse; |
| 20 | +import com.slack.api.methods.response.chat.ChatPostMessageResponse; |
| 21 | +import com.slack.api.methods.response.chat.ChatUpdateResponse; |
| 22 | +import com.slack.api.methods.response.conversations.ConversationsHistoryResponse; |
| 23 | +import com.slack.api.methods.response.conversations.ConversationsListResponse; |
| 24 | +import com.slack.api.methods.response.conversations.ConversationsRepliesResponse; |
| 25 | +import com.slack.api.methods.response.files.FilesInfoResponse; |
| 26 | +import com.slack.api.methods.response.files.FilesUploadV2Response; |
| 27 | +import com.slack.api.methods.response.team.TeamInfoResponse; |
| 28 | +import com.slack.api.methods.response.team.TeamPreferencesListResponse; |
| 29 | +import com.slack.api.methods.response.team.profile.TeamProfileGetResponse; |
| 30 | +import com.slack.api.methods.response.users.UsersListResponse; |
| 31 | +import com.slack.api.methods.response.users.profile.UsersProfileGetResponse; |
| 32 | +import com.slack.api.model.Conversation; |
| 33 | +import com.slack.api.model.User; |
| 34 | +import com.slack.api.model.canvas.*; |
| 35 | +import config.Constants; |
| 36 | +import config.SlackTestConfig; |
| 37 | +import lombok.extern.slf4j.Slf4j; |
| 38 | +import org.junit.AfterClass; |
| 39 | +import org.junit.Before; |
| 40 | +import org.junit.BeforeClass; |
| 41 | +import org.junit.Test; |
| 42 | + |
| 43 | +import java.io.File; |
| 44 | +import java.io.IOException; |
| 45 | +import java.util.ArrayList; |
| 46 | +import java.util.Collections; |
| 47 | +import java.util.List; |
| 48 | + |
| 49 | +import static java.util.stream.Collectors.toList; |
| 50 | +import static org.hamcrest.CoreMatchers.*; |
| 51 | +import static org.hamcrest.MatcherAssert.assertThat; |
| 52 | +import static org.hamcrest.Matchers.greaterThan; |
| 53 | + |
| 54 | +/** |
| 55 | + * The purpose of this test suite is to detect important property updates in API responses. |
| 56 | + * In particular, the following elements are often quietly added to production API responses, |
| 57 | + * and the lack of these properties can affect developers so much: |
| 58 | + * - Block Kit components |
| 59 | + * - File object metadata |
| 60 | + * - User info details |
| 61 | + * Therefore, regularly running these tests will help quickly identify any changes. |
| 62 | + * <p> |
| 63 | + * To execute this test suite, you need to create a sandbox environment |
| 64 | + * and install your custom app with appropriate bot scopes. |
| 65 | + * <p> |
| 66 | + * For future updates, you can reuse the code under `test_with_remote_apis.methods` as needed. |
| 67 | + * It's okay if it's not perfect initially; you can gradually enhance your test assets over time. |
| 68 | + */ |
| 69 | +@Slf4j |
| 70 | +public class MinimumPropertyDetectionTest { |
| 71 | + |
| 72 | + // Setting this env variable is required; |
| 73 | + // If you want to go with a different env variable name, feel free to go with it |
| 74 | + static String botToken = System.getenv(Constants.SLACK_SDK_TEST_BOT_TOKEN); |
| 75 | + |
| 76 | + // This TestConfig enables this test execution to detect missing properties in Java code |
| 77 | + static SlackTestConfig testConfig = SlackTestConfig.getInstance(); |
| 78 | + static Slack slack = Slack.getInstance(testConfig.getConfig()); |
| 79 | + // Use this Web API client for testing |
| 80 | + static MethodsClient client = slack.methods(botToken); |
| 81 | + // You can use this async client to avoid failing with a "ratelimited" error |
| 82 | + static AsyncMethodsClient asyncClient = slack.methodsAsync(botToken); |
| 83 | + |
| 84 | + @BeforeClass |
| 85 | + public static void setUp() throws Exception { |
| 86 | + // Usually these invocations do nothing; check the code to learn what it does |
| 87 | + SlackTestConfig.initializeRawJSONDataFiles("bookmarks.*"); |
| 88 | + SlackTestConfig.initializeRawJSONDataFiles("bots.*"); |
| 89 | + SlackTestConfig.initializeRawJSONDataFiles("canvases.*"); |
| 90 | + SlackTestConfig.initializeRawJSONDataFiles("chat.*"); |
| 91 | + SlackTestConfig.initializeRawJSONDataFiles("conversations.*"); |
| 92 | + SlackTestConfig.initializeRawJSONDataFiles("files.*"); |
| 93 | + SlackTestConfig.initializeRawJSONDataFiles("team.*"); |
| 94 | + SlackTestConfig.initializeRawJSONDataFiles("usergroups.*"); |
| 95 | + SlackTestConfig.initializeRawJSONDataFiles("users.*"); |
| 96 | + } |
| 97 | + |
| 98 | + @Before |
| 99 | + public void beforeEachTest() throws Exception { |
| 100 | + // Changing this method for optimizing the test performance is totally fine |
| 101 | + // The fastest approach would be to have the channel ID as an env variable |
| 102 | + loadTestChannelId(); |
| 103 | + } |
| 104 | + |
| 105 | + @AfterClass |
| 106 | + public static void tearDown() throws InterruptedException { |
| 107 | + // This part finalizes the generated JSON files; there may be some rooms for improvements |
| 108 | + SlackTestConfig.awaitCompletion(testConfig); |
| 109 | + } |
| 110 | + |
| 111 | + // If #random does not work for your testing workspace, replacing this name with a different one is totally fine |
| 112 | + private static final String TEST_CHANNEL_NAME = "random"; |
| 113 | + private String testChannelId = null; |
| 114 | + |
| 115 | + void loadTestChannelId() throws IOException, SlackApiException { |
| 116 | + if (testChannelId == null) { |
| 117 | + ConversationsListResponse channels = slack.methods().conversationsList(r -> r |
| 118 | + .token(botToken) |
| 119 | + .excludeArchived(true) |
| 120 | + .limit(100) |
| 121 | + ); |
| 122 | + assertThat(channels.getError(), is(nullValue())); |
| 123 | + |
| 124 | + for (Conversation channel : channels.getChannels()) { |
| 125 | + if (channel.getName().equals(TEST_CHANNEL_NAME)) { |
| 126 | + testChannelId = channel.getId(); |
| 127 | + break; |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + // ----------------------------------------------------------------------------------------- |
| 134 | + // When you run the following test methods and the returned API response has unknown properties, |
| 135 | + // The test fails with an exception; check the error message to know what's missing in current Java code |
| 136 | + // ----------------------------------------------------------------------------------------- |
| 137 | + |
| 138 | + @Test |
| 139 | + public void bookmarks() throws Exception { |
| 140 | + BookmarksListResponse bookmarks = client.bookmarksList(r -> r |
| 141 | + .channelId(testChannelId) |
| 142 | + ); |
| 143 | + assertThat(bookmarks.getError(), is(nullValue())); |
| 144 | + |
| 145 | + ChatPostMessageResponse message = client.chatPostMessage(r -> r |
| 146 | + .channel(testChannelId) |
| 147 | + .text("A very important message!")); |
| 148 | + assertThat(message.getError(), is(nullValue())); |
| 149 | + |
| 150 | + String permalink = client.chatGetPermalink(r -> r |
| 151 | + .channel(testChannelId) |
| 152 | + .messageTs(message.getTs()) |
| 153 | + ).getPermalink(); |
| 154 | + |
| 155 | + BookmarksAddResponse creation = client.bookmarksAdd(req -> req |
| 156 | + .channelId(testChannelId) |
| 157 | + .title("test") |
| 158 | + .link(permalink) |
| 159 | + .type("link") |
| 160 | + ); |
| 161 | + assertThat(creation.getError(), is(nullValue())); |
| 162 | + |
| 163 | + BookmarksEditResponse modification = client.bookmarksEdit(req -> req |
| 164 | + .channelId(testChannelId) |
| 165 | + .bookmarkId(creation.getBookmark().getId()) |
| 166 | + .title("test") |
| 167 | + .link(permalink) |
| 168 | + ); |
| 169 | + assertThat(modification.getError(), is(nullValue())); |
| 170 | + |
| 171 | + BookmarksRemoveResponse removal = client.bookmarksRemove(req -> req |
| 172 | + .channelId(testChannelId) |
| 173 | + .bookmarkId(modification.getBookmark().getId()) |
| 174 | + ); |
| 175 | + assertThat(removal.getError(), is(nullValue())); |
| 176 | + } |
| 177 | + |
| 178 | + @Test |
| 179 | + public void bots() throws Exception { |
| 180 | + User botUser = null; |
| 181 | + String cursor = null; |
| 182 | + while (botUser == null && (cursor == null || !cursor.isEmpty())) { |
| 183 | + // using async client to prevent failing due to a rate limited error |
| 184 | + UsersListResponse response = asyncClient.usersList(req -> req).get(); |
| 185 | + for (User u : response.getMembers()) { |
| 186 | + if (u.isBot() && !"USLACKBOT".equals(u.getId())) { |
| 187 | + botUser = u; |
| 188 | + break; |
| 189 | + } |
| 190 | + } |
| 191 | + if (response.getResponseMetadata() != null) { |
| 192 | + cursor = response.getResponseMetadata().getNextCursor(); |
| 193 | + } |
| 194 | + } |
| 195 | + assertThat(botUser, is(notNullValue())); |
| 196 | + |
| 197 | + String botId = botUser.getProfile().getBotId(); |
| 198 | + BotsInfoResponse response = client.botsInfo(req -> req.bot(botId)); |
| 199 | + assertThat(response.getError(), is(nullValue())); |
| 200 | + } |
| 201 | + |
| 202 | + @Test |
| 203 | + public void canvases() throws Exception { |
| 204 | + CanvasesCreateResponse creation = client.canvasesCreate(r -> r |
| 205 | + .title("My canvas " + System.currentTimeMillis()) |
| 206 | + .documentContent(CanvasDocumentContent.builder().markdown( |
| 207 | + "# Standalone canvas document\n" + |
| 208 | + "\n" + |
| 209 | + "---\n" + |
| 210 | + "## Before\n" + |
| 211 | + "**foo** _bar_\n" + |
| 212 | + "hey hey\n" + |
| 213 | + "\n").build()) |
| 214 | + ); |
| 215 | + assertThat(creation.getError(), is(nullValue())); |
| 216 | + |
| 217 | + String canvasId = creation.getCanvasId(); |
| 218 | + try { |
| 219 | + List<String> userIds = Collections.singletonList(client.authTest(r -> r).getUserId()); |
| 220 | + CanvasesSectionsLookupResponse lookupResult = client.canvasesSectionsLookup(r -> r |
| 221 | + .canvasId(canvasId) |
| 222 | + .criteria(CanvasesSectionsLookupRequest.Criteria.builder() |
| 223 | + .sectionTypes(Collections.singletonList(CanvasDocumentSectionType.H2)) |
| 224 | + .containsText("Before") |
| 225 | + .build() |
| 226 | + ) |
| 227 | + ); |
| 228 | + assertThat(lookupResult.getError(), is(nullValue())); |
| 229 | + |
| 230 | + String sectionId = lookupResult.getSections().get(0).getId(); |
| 231 | + CanvasesEditResponse edit = client.canvasesEdit(r -> r |
| 232 | + .canvasId(canvasId) |
| 233 | + .changes(Collections.singletonList(CanvasDocumentChange.builder() |
| 234 | + .sectionId(sectionId) |
| 235 | + .operation(CanvasEditOperation.REPLACE) |
| 236 | + .documentContent(CanvasDocumentContent.builder().markdown("## After").build()) |
| 237 | + .build() |
| 238 | + )) |
| 239 | + ); |
| 240 | + assertThat(edit.getError(), is(nullValue())); |
| 241 | + |
| 242 | + FilesInfoResponse details = client.filesInfo(r -> r.file(canvasId)); |
| 243 | + assertThat(details.getError(), is(nullValue())); |
| 244 | + |
| 245 | + CanvasesAccessSetResponse set = client.canvasesAccessSet(r -> r |
| 246 | + .canvasId(canvasId) |
| 247 | + .accessLevel(CanvasDocumentAccessLevel.WRITE) |
| 248 | + .userIds(userIds) |
| 249 | + ); |
| 250 | + assertThat(set.getError(), is(nullValue())); |
| 251 | + CanvasesAccessDeleteResponse delete = client.canvasesAccessDelete(r -> r |
| 252 | + .canvasId(canvasId) |
| 253 | + .userIds(userIds) |
| 254 | + ); |
| 255 | + assertThat(delete.getError(), is(nullValue())); |
| 256 | + |
| 257 | + } finally { |
| 258 | + CanvasesDeleteResponse deletion = client.canvasesDelete(r -> r.canvasId(canvasId)); |
| 259 | + assertThat(deletion.getError(), is(nullValue())); |
| 260 | + } |
| 261 | + } |
| 262 | + |
| 263 | + @Test |
| 264 | + public void chat() throws Exception { |
| 265 | + String text = "This is a _test_ message posted by `test_with_remote_apis.MinimumPropertyDetectionTest`"; |
| 266 | + ChatPostMessageResponse newMessage = client.chatPostMessage(req -> req |
| 267 | + .channel(testChannelId) |
| 268 | + .text(text) |
| 269 | + ); |
| 270 | + assertThat(newMessage.getError(), is(nullValue())); |
| 271 | + assertThat(newMessage.getMessage().getText(), is(text)); |
| 272 | + |
| 273 | + String messageTs = newMessage.getTs(); |
| 274 | + |
| 275 | + ConversationsHistoryResponse history = client.conversationsHistory(req -> req |
| 276 | + .channel(testChannelId) |
| 277 | + .latest(messageTs) |
| 278 | + .inclusive(true) |
| 279 | + ); |
| 280 | + assertThat(history.getError(), is(nullValue())); |
| 281 | + |
| 282 | + ConversationsRepliesResponse replies = client.conversationsReplies(req -> req |
| 283 | + .channel(testChannelId) |
| 284 | + .ts(messageTs) |
| 285 | + .inclusive(true) |
| 286 | + ); |
| 287 | + assertThat(replies.getError(), is(nullValue())); |
| 288 | + |
| 289 | + ChatUpdateResponse modification = client.chatUpdate(req -> req |
| 290 | + .channel(testChannelId) |
| 291 | + .ts(messageTs) |
| 292 | + .text("**EDIT:** " + text) |
| 293 | + ); |
| 294 | + assertThat(modification.getError(), is(nullValue())); |
| 295 | + |
| 296 | + ChatDeleteResponse deletion = client.chatDelete(req -> req.channel(testChannelId).ts(messageTs)); |
| 297 | + assertThat(deletion.getError(), is(nullValue())); |
| 298 | + } |
| 299 | + |
| 300 | + @Test |
| 301 | + public void files() throws Exception { |
| 302 | + File file = new File("src/test/resources/sample_long.txt"); |
| 303 | + FilesUploadV2Response upload = client.filesUploadV2(r -> r |
| 304 | + .file(file) |
| 305 | + .channel(testChannelId) |
| 306 | + .initialComment("initial comment") |
| 307 | + .filename("sample.txt") |
| 308 | + .title("file title") |
| 309 | + ); |
| 310 | + assertThat(upload.getError(), is(nullValue())); |
| 311 | + assertThat(upload.isOk(), is(true)); |
| 312 | + assertThat(upload.getFile().getTitle(), is("file title")); |
| 313 | + assertThat(upload.getFile().getName(), is("sample.txt")); |
| 314 | + |
| 315 | + com.slack.api.model.File fileObj = null; |
| 316 | + while (fileObj == null || fileObj.getShares() == null || fileObj.getShares().getPublicChannels() == null) { |
| 317 | + fileObj = client.filesInfo(r -> r.file(upload.getFile().getId())).getFile(); |
| 318 | + Thread.sleep(1_000L); |
| 319 | + } |
| 320 | + assertThat(fileObj.getTitle(), is("file title")); |
| 321 | + assertThat(fileObj.getName(), is("sample.txt")); |
| 322 | + |
| 323 | + String messageTs = fileObj.getShares().getPublicChannels().get(testChannelId).get(0).getTs(); |
| 324 | + ChatDeleteResponse response = slack.methods(botToken).chatDelete(r -> r |
| 325 | + .channel(testChannelId) |
| 326 | + .ts(messageTs) |
| 327 | + ); |
| 328 | + assertThat(response.getError(), is(nullValue())); |
| 329 | + assertThat(response.isOk(), is(true)); |
| 330 | + } |
| 331 | + |
| 332 | + |
| 333 | + @Test |
| 334 | + public void team() throws Exception { |
| 335 | + TeamInfoResponse team = client.teamInfo(r -> r); |
| 336 | + assertThat(team.getError(), is(nullValue())); |
| 337 | + |
| 338 | + String teamId = team.getTeam().getId(); |
| 339 | + |
| 340 | + TeamProfileGetResponse profile = client.teamProfileGet(r -> r.teamId(teamId)); |
| 341 | + assertThat(profile.getError(), is(nullValue())); |
| 342 | + |
| 343 | + TeamPreferencesListResponse preferences = client.teamPreferencesList(r -> r); |
| 344 | + assertThat(preferences.getError(), is(nullValue())); |
| 345 | + } |
| 346 | + |
| 347 | + @Test |
| 348 | + public void users_profile() throws Exception { |
| 349 | + User humanUser = null; |
| 350 | + // Using async client to avoid an exception due to rate limited errors |
| 351 | + for (User user : asyncClient.usersList(r -> r).get().getMembers()) { |
| 352 | + if (!user.isBot() && !user.isAppUser() && !user.isStranger() && !user.isDeleted()) { |
| 353 | + humanUser = user; |
| 354 | + break; |
| 355 | + } |
| 356 | + } |
| 357 | + String humanUserId = humanUser.getId(); |
| 358 | + UsersProfileGetResponse profileGet = client.usersProfileGet(r -> r.user(humanUserId)); |
| 359 | + assertThat(profileGet.getError(), is(nullValue())); |
| 360 | + } |
| 361 | + |
| 362 | + @Test |
| 363 | + public void users() throws Exception { |
| 364 | + // Scanning all users is useful to detect optional property existence |
| 365 | + List<String> userIds = new ArrayList<>(); |
| 366 | + String nextCursor = null; |
| 367 | + while (nextCursor == null || !nextCursor.isEmpty()) { |
| 368 | + // Using async client to avoid an exception due to rate limited errors |
| 369 | + UsersListResponse response = asyncClient.usersList(r -> r |
| 370 | + .includeLocale(true) |
| 371 | + .limit(3000) |
| 372 | + ).get(); |
| 373 | + nextCursor = response.getResponseMetadata().getNextCursor(); |
| 374 | + userIds.addAll(response.getMembers().stream().map(User::getId).collect(toList())); |
| 375 | + } |
| 376 | + assertThat(userIds.size(), is(greaterThan(0))); |
| 377 | + } |
| 378 | +} |
0 commit comments