diff --git a/.gitignore b/.gitignore index d754be6..5df2901 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules .cache .DS_Store .idea/ +*.iml diff --git a/__tests__/__fixtures__/reviewMessage.ts b/__tests__/__fixtures__/reviewMessage.ts index 3247a24..d809a3c 100644 --- a/__tests__/__fixtures__/reviewMessage.ts +++ b/__tests__/__fixtures__/reviewMessage.ts @@ -10,6 +10,13 @@ export const reviewMessagePostFixture = { accessory: { action_id: 'review-message-actions', options: [ + { + text: { + type: 'plain_text', + text: 'Refresh', + }, + value: 'review-refresh~~1148~~1', + }, { text: { type: 'plain_text', diff --git a/__tests__/review/refreshReview.test.ts b/__tests__/review/refreshReview.test.ts new file mode 100644 index 0000000..96e70f8 --- /dev/null +++ b/__tests__/review/refreshReview.test.ts @@ -0,0 +1,109 @@ +import request from 'supertest'; +import { app } from '@/app'; +import { HTTP_STATUS_OK } from '@/constants'; +import { addReviewToChannel } from '@/core/services/data'; +import { slackBotWebClient } from '@/core/services/slack'; +import { mergeRequestDetailsFixture } from '../__fixtures__/mergeRequestDetailsFixture'; +import { mergeRequestFixture } from '../__fixtures__/mergeRequestFixture'; +import { getSlackHeaders } from '../utils/getSlackHeaders'; +import { mockBuildReviewMessageCalls } from '../utils/mockBuildReviewMessageCalls'; +import { mockGitlabCall } from '../utils/mockGitlabCall'; + +describe('review > refreshReview', () => { + const channelId = 'channelId'; + const mergeRequestIid = mergeRequestFixture.iid; + const projectId = mergeRequestFixture.project_id; + const ts = 'ts'; + const userId = 'userId'; + + const buildBody = () => ({ + payload: JSON.stringify({ + actions: [ + { + action_id: 'review-message-actions', + selected_option: { + value: `review-refresh~~${projectId}~~${mergeRequestIid}`, + }, + }, + ], + channel: { id: channelId }, + message: { ts }, + type: 'block_actions', + user: { id: userId }, + }), + }); + + const permalink = + 'https://manomano-team.slack.com/archives/CKXA1FASF/p1640343776000900'; + + beforeEach(() => { + (slackBotWebClient.users.lookupByEmail as jest.Mock).mockImplementation( + ({ email }: { email: string }) => { + const name = email.split('@')[0]; + return Promise.resolve({ + user: { + name, + profile: { image_72: 'image_72' }, + real_name: `${name}.real`, + }, + }); + }, + ); + (slackBotWebClient.chat.update as jest.Mock).mockResolvedValue({}); + (slackBotWebClient.chat.getPermalink as jest.Mock).mockResolvedValue({ + permalink, + }); + }); + + it('should refresh the review message and keep it in DB when MR is merged', async () => { + mockBuildReviewMessageCalls(); + mockGitlabCall(`/projects/${projectId}/merge_requests/${mergeRequestIid}`, { + ...mergeRequestDetailsFixture, + state: 'merged', + }); + + await addReviewToChannel({ channelId, mergeRequestIid, projectId, ts }); + const body = buildBody(); + + const response = await request(app) + .post('/api/v1/homer/interactive') + .set(getSlackHeaders(body)) + .send(body); + + const { hasModelEntry } = (await import('sequelize')) as any; + expect(response.status).toEqual(HTTP_STATUS_OK); + expect(slackBotWebClient.chat.update).toHaveBeenCalled(); + expect(slackBotWebClient.chat.postEphemeral).toHaveBeenNthCalledWith(1, { + channel: channelId, + user: userId, + text: expect.stringContaining(permalink), + }); + expect( + await hasModelEntry('Review', { channelId, mergeRequestIid, ts }), + ).toEqual(true); + }); + + it('should refresh the review message and keep it in DB when MR is still open', async () => { + mockBuildReviewMessageCalls(); // mergeRequestDetailsFixture has state: 'opened' + + await addReviewToChannel({ channelId, mergeRequestIid, projectId, ts }); + const body = buildBody(); + + const response = await request(app) + .post('/api/v1/homer/interactive') + .set(getSlackHeaders(body)) + .send(body); + + const { hasModelEntry } = (await import('sequelize')) as any; + expect(response.status).toEqual(HTTP_STATUS_OK); + expect(slackBotWebClient.chat.update).toHaveBeenCalled(); + expect(slackBotWebClient.chat.postEphemeral).toHaveBeenNthCalledWith(1, { + channel: channelId, + user: userId, + text: expect.stringContaining(permalink), + }); + expect( + await hasModelEntry('Review', { channelId, mergeRequestIid, ts }), + ).toEqual(true); + }); +}); diff --git a/src/review/commands/share/utils/handleMessageActions.ts b/src/review/commands/share/utils/handleMessageActions.ts index 71ae0df..e40b41d 100644 --- a/src/review/commands/share/utils/handleMessageActions.ts +++ b/src/review/commands/share/utils/handleMessageActions.ts @@ -5,10 +5,11 @@ import type { BlockActionsPayloadWithChannel } from '@/core/typings/BlockActionP import type { StaticSelectAction } from '@/core/typings/StaticSelectAction'; import { createPipeline } from './createPipeline'; import { rebaseSourceBranch } from './rebaseSourceBranch'; +import { refreshReview } from './refreshReview'; export async function handleMessageAction( payload: BlockActionsPayloadWithChannel, - action: StaticSelectAction + action: StaticSelectAction, ) { const mergeRequestAction = action.selected_option.value; @@ -22,9 +23,11 @@ export async function handleMessageAction( await removeReview(ts); } else if (mergeRequestAction.startsWith('review-rebase-source-branch')) { await rebaseSourceBranch(payload, action); + } else if (mergeRequestAction.startsWith('review-refresh')) { + await refreshReview(payload, action); } else { logger.error( - new Error(`Unknown review message action: ${mergeRequestAction}.`) + new Error(`Unknown review message action: ${mergeRequestAction}.`), ); } } diff --git a/src/review/commands/share/utils/refreshReview.ts b/src/review/commands/share/utils/refreshReview.ts new file mode 100644 index 0000000..3d309e2 --- /dev/null +++ b/src/review/commands/share/utils/refreshReview.ts @@ -0,0 +1,42 @@ +import { logger } from '@/core/services/logger'; +import { getPermalink, slackBotWebClient } from '@/core/services/slack'; +import type { BlockActionsPayloadWithChannel } from '@/core/typings/BlockActionPayload'; +import type { StaticSelectAction } from '@/core/typings/StaticSelectAction'; +import { extractActionParameters } from '@/core/utils/slackActions'; +import { buildReviewMessage } from '../viewBuilders/buildReviewMessage'; + +export async function refreshReview( + payload: BlockActionsPayloadWithChannel, + action: StaticSelectAction, +) { + const mergeRequestAction = action.selected_option.value; + const { channel, user, message } = payload; + const [projectIdStr, mergeRequestIidStr] = + extractActionParameters(mergeRequestAction); + + if (!projectIdStr || !mergeRequestIidStr) { + logger.error( + new Error( + `Unable to get projectId and mergeRequestIid for action ${mergeRequestAction}.`, + ), + ); + return; + } + + const projectId = parseInt(projectIdStr, 10); + const mergeRequestIid = parseInt(mergeRequestIidStr, 10); + const { ts } = message; + + const [reviewMessage, permalink] = await Promise.all([ + buildReviewMessage(channel.id, projectId, mergeRequestIid, ts), + getPermalink(channel.id, ts), + ]); + + await slackBotWebClient.chat.update(reviewMessage); + + await slackBotWebClient.chat.postEphemeral({ + channel: channel.id, + user: user.id, + text: `Review message refreshed :homer-happy: ${permalink}`, + }); +} diff --git a/src/review/commands/share/viewBuilders/buildReviewMessage.ts b/src/review/commands/share/viewBuilders/buildReviewMessage.ts index c0d263a..2208fe6 100644 --- a/src/review/commands/share/viewBuilders/buildReviewMessage.ts +++ b/src/review/commands/share/viewBuilders/buildReviewMessage.ts @@ -135,6 +135,14 @@ function buildHeaderBlock( type: 'overflow', action_id: 'review-message-actions', options: [ + { + text: { type: 'plain_text', text: 'Refresh' }, + value: injectActionsParameters( + 'review-refresh', + projectId, + mergeRequest.iid, + ), + }, { text: { type: 'plain_text', text: 'Create a pipeline' }, value: injectActionsParameters(