2022-05-12 11:28:55 +01:00
|
|
|
import { createMessageQueue } from "../../src/MessageQueue";
|
2024-04-08 15:22:57 +01:00
|
|
|
import { UserTokenStore } from "../../src/tokens/UserTokenStore";
|
2022-05-12 11:28:55 +01:00
|
|
|
import { AppserviceMock } from "../utils/AppserviceMock";
|
|
|
|
import { ApiError, ErrCode, ValidatorApiError } from "../../src/api";
|
|
|
|
import { GitLabRepoConnection, GitLabRepoConnectionState } from "../../src/Connections";
|
2023-01-10 17:08:50 +00:00
|
|
|
import { expect } from "chai";
|
2023-05-18 11:38:59 +01:00
|
|
|
import { BridgeConfigGitLab } from "../../src/config/Config";
|
2023-08-14 14:58:21 +02:00
|
|
|
import { IBridgeStorageProvider } from "../../src/Stores/StorageProvider";
|
|
|
|
import { IntentMock } from "../utils/IntentMock";
|
2025-02-25 12:12:49 +00:00
|
|
|
import { IGitlabMergeRequest, IGitlabProject, IGitlabUser, IGitLabWebhookNoteEvent } from "../../src/Gitlab/WebhookTypes";
|
2022-05-12 11:28:55 +01:00
|
|
|
|
|
|
|
const ROOM_ID = "!foo:bar";
|
|
|
|
|
|
|
|
const GITLAB_ORG_REPO = {
|
|
|
|
org: "a-fake-org",
|
|
|
|
repo: "a-fake-repo",
|
|
|
|
};
|
|
|
|
|
2025-02-25 12:12:49 +00:00
|
|
|
const GITLAB_MR: IGitlabMergeRequest = {
|
|
|
|
author_id: 0,
|
|
|
|
labels: [],
|
2022-05-12 11:28:55 +01:00
|
|
|
state: "opened",
|
|
|
|
iid: 1234,
|
|
|
|
url: `https://gitlab.example.com/${GITLAB_ORG_REPO.org}/${GITLAB_ORG_REPO.repo}/issues/1234`,
|
|
|
|
title: "My MR",
|
|
|
|
};
|
|
|
|
|
2025-02-25 12:12:49 +00:00
|
|
|
const GITLAB_USER: IGitlabUser = {
|
2023-08-14 14:58:21 +02:00
|
|
|
name: "Alice",
|
|
|
|
username: "alice",
|
2025-02-25 12:12:49 +00:00
|
|
|
avatar_url: "",
|
|
|
|
email: "alice@example.org"
|
2023-08-14 14:58:21 +02:00
|
|
|
};
|
|
|
|
|
2025-02-25 12:12:49 +00:00
|
|
|
const GITLAB_PROJECT: IGitlabProject = {
|
2023-08-14 14:58:21 +02:00
|
|
|
path_with_namespace: `${GITLAB_ORG_REPO.org}/${GITLAB_ORG_REPO.repo}`,
|
|
|
|
web_url: `https://gitlab.example.com/${GITLAB_ORG_REPO.org}/${GITLAB_ORG_REPO.repo}`,
|
2025-02-25 12:12:49 +00:00
|
|
|
homepage: "",
|
2023-08-14 14:58:21 +02:00
|
|
|
};
|
|
|
|
|
2022-05-12 11:28:55 +01:00
|
|
|
const GITLAB_ISSUE_CREATED_PAYLOAD = {
|
|
|
|
object_kind: "merge_request",
|
2023-08-14 14:58:21 +02:00
|
|
|
user: GITLAB_USER,
|
2022-05-12 11:28:55 +01:00
|
|
|
object_attributes: GITLAB_MR,
|
2023-08-14 14:58:21 +02:00
|
|
|
project: GITLAB_PROJECT,
|
|
|
|
};
|
|
|
|
|
2025-02-25 12:12:49 +00:00
|
|
|
const GITLAB_MR_COMMENT: IGitLabWebhookNoteEvent = {
|
2023-08-14 14:58:21 +02:00
|
|
|
'object_kind': 'note',
|
|
|
|
'event_type': 'note',
|
|
|
|
'merge_request': GITLAB_MR,
|
|
|
|
'object_attributes': {
|
|
|
|
'discussion_id': '6babfc4ad3be2355db286ed50be111a5220d5751',
|
|
|
|
'note': 'I am starting a new thread',
|
|
|
|
'noteable_type': 'MergeRequest',
|
2025-02-25 12:12:49 +00:00
|
|
|
'url': 'https://gitlab.com/tadeuszs/my-awesome-project/-/merge_requests/2#note_1455087141',
|
|
|
|
'id': 1455087141,
|
|
|
|
'author_id': 12345,
|
|
|
|
'noteable_id': 1,
|
2023-08-14 14:58:21 +02:00
|
|
|
},
|
|
|
|
'project': GITLAB_PROJECT,
|
|
|
|
'user': GITLAB_USER,
|
2025-02-25 12:12:49 +00:00
|
|
|
repository: {
|
|
|
|
'description': 'A repo',
|
|
|
|
'homepage': 'https://gitlab.com/tadeuszs/my-awesome-project',
|
|
|
|
'name': 'a-repo',
|
|
|
|
'url': 'https://gitlab.com/tadeuszs/my-awesome-project'
|
|
|
|
},
|
2022-05-12 11:28:55 +01:00
|
|
|
};
|
|
|
|
|
2023-08-14 14:58:21 +02:00
|
|
|
const COMMENT_DEBOUNCE_MS = 25;
|
|
|
|
|
2025-02-25 12:12:49 +00:00
|
|
|
function createConnection(state: Partial<GitLabRepoConnectionState> = {}, isExistingState=false): { connection: GitLabRepoConnection, intent: IntentMock } {
|
2024-03-19 16:45:52 +00:00
|
|
|
const mq = createMessageQueue();
|
2022-05-12 11:28:55 +01:00
|
|
|
mq.subscribe('*');
|
|
|
|
const as = AppserviceMock.create();
|
2023-01-13 10:32:09 -05:00
|
|
|
const intent = as.getIntentForUserId('@gitlab:example.test');
|
2022-05-12 11:28:55 +01:00
|
|
|
const connection = new GitLabRepoConnection(
|
|
|
|
ROOM_ID,
|
|
|
|
"state_key",
|
2023-03-17 16:38:39 +00:00
|
|
|
as,
|
2023-08-14 14:58:21 +02:00
|
|
|
{
|
|
|
|
commentDebounceMs: COMMENT_DEBOUNCE_MS,
|
|
|
|
} as BridgeConfigGitLab,
|
2023-01-13 10:32:09 -05:00
|
|
|
intent,
|
2022-05-12 11:28:55 +01:00
|
|
|
GitLabRepoConnection.validateState({
|
|
|
|
instance: "bar",
|
|
|
|
path: "foo",
|
|
|
|
...state,
|
|
|
|
}, isExistingState),
|
|
|
|
{} as UserTokenStore,
|
|
|
|
{
|
|
|
|
url: "https://gitlab.example.com"
|
|
|
|
},
|
2023-08-14 14:58:21 +02:00
|
|
|
{
|
|
|
|
setGitlabDiscussionThreads: () => Promise.resolve(),
|
|
|
|
getGitlabDiscussionThreads: () => Promise.resolve([]),
|
|
|
|
} as unknown as IBridgeStorageProvider,
|
2022-05-12 11:28:55 +01:00
|
|
|
);
|
2023-01-13 10:32:09 -05:00
|
|
|
return {connection, intent};
|
2022-05-12 11:28:55 +01:00
|
|
|
}
|
|
|
|
|
2023-08-14 14:58:21 +02:00
|
|
|
async function waitForDebouncing(): Promise<void> {
|
|
|
|
return new Promise(resolve => setTimeout(resolve, COMMENT_DEBOUNCE_MS * 2));
|
|
|
|
}
|
|
|
|
|
2022-05-12 11:28:55 +01:00
|
|
|
describe("GitLabRepoConnection", () => {
|
|
|
|
describe("validateState", () => {
|
|
|
|
it("can validate a completes state config", () => {
|
|
|
|
GitLabRepoConnection.validateState({
|
|
|
|
instance: "foo",
|
|
|
|
path: "bar/baz",
|
2023-01-10 17:08:50 +00:00
|
|
|
enableHooks: [
|
2022-05-12 11:28:55 +01:00
|
|
|
"merge_request.open",
|
2024-05-22 13:05:14 +02:00
|
|
|
"merge_request.reopen",
|
2022-05-12 11:28:55 +01:00
|
|
|
"merge_request.close",
|
|
|
|
"merge_request.merge",
|
|
|
|
"merge_request.review",
|
|
|
|
"merge_request.review.comments",
|
|
|
|
"merge_request",
|
|
|
|
"tag_push",
|
|
|
|
"push",
|
|
|
|
"wiki",
|
|
|
|
"release",
|
|
|
|
"release.created",
|
|
|
|
],
|
|
|
|
commandPrefix: "!gl",
|
|
|
|
pushTagsRegex: ".*",
|
|
|
|
includingLabels: ["me"],
|
|
|
|
excludingLabels: ["but-not-me"],
|
|
|
|
} as GitLabRepoConnectionState as unknown as Record<string, unknown>);
|
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2023-01-10 17:08:50 +00:00
|
|
|
it("will convert ignoredHooks for existing state", () => {
|
|
|
|
const state = GitLabRepoConnection.validateState({
|
|
|
|
instance: "foo",
|
|
|
|
path: "bar/baz",
|
|
|
|
ignoreHooks: [
|
|
|
|
"merge_request",
|
|
|
|
],
|
|
|
|
commandPrefix: "!gl",
|
|
|
|
} as GitLabRepoConnectionState as unknown as Record<string, unknown>, true);
|
|
|
|
expect(state.enableHooks).to.not.contain('merge_request');
|
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2022-05-12 11:28:55 +01:00
|
|
|
it("will disallow invalid state", () => {
|
|
|
|
try {
|
|
|
|
GitLabRepoConnection.validateState({
|
|
|
|
instance: "foo",
|
|
|
|
path: 123,
|
|
|
|
});
|
|
|
|
} catch (ex) {
|
|
|
|
if (ex instanceof ValidatorApiError === false || ex.errcode !== ErrCode.BadValue) {
|
|
|
|
throw ex;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2023-01-10 17:08:50 +00:00
|
|
|
it("will disallow enabledHooks to contains invalid enums if this is new state", () => {
|
2022-05-12 11:28:55 +01:00
|
|
|
try {
|
|
|
|
GitLabRepoConnection.validateState({
|
|
|
|
instance: "bar",
|
|
|
|
path: "foo",
|
2023-01-10 17:08:50 +00:00
|
|
|
enabledHooks: ["not-real"],
|
2022-05-12 11:28:55 +01:00
|
|
|
}, false);
|
|
|
|
} catch (ex) {
|
|
|
|
if (ex instanceof ApiError === false || ex.errcode !== ErrCode.BadValue) {
|
|
|
|
throw ex;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2023-01-10 17:08:50 +00:00
|
|
|
it("will allow enabledHooks to contains invalid enums if this is old state", () => {
|
2022-05-12 11:28:55 +01:00
|
|
|
GitLabRepoConnection.validateState({
|
|
|
|
instance: "bar",
|
|
|
|
path: "foo",
|
2023-01-10 17:08:50 +00:00
|
|
|
enabledHooks: ["not-real"],
|
2022-05-12 11:28:55 +01:00
|
|
|
}, true);
|
|
|
|
});
|
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2025-02-25 12:12:49 +00:00
|
|
|
describe("onMergeRequestCommentCreated", () => {
|
2023-08-14 14:58:21 +02:00
|
|
|
it("will handle an MR comment", async () => {
|
|
|
|
const { connection, intent } = createConnection();
|
2025-02-25 12:12:49 +00:00
|
|
|
await connection.onMergeRequestCommentCreated(GITLAB_MR_COMMENT);
|
2023-08-14 14:58:21 +02:00
|
|
|
await waitForDebouncing();
|
|
|
|
intent.expectEventMatches(
|
|
|
|
(ev: any) => ev.content.body.includes('**Alice** commented on MR'),
|
|
|
|
'event body indicates MR comment'
|
|
|
|
);
|
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2025-02-25 12:12:49 +00:00
|
|
|
it("will filter out issues not matching includingLabels.", async () => {
|
|
|
|
const { connection, intent } = createConnection({
|
|
|
|
includingLabels: ["include-me"]
|
|
|
|
});
|
|
|
|
// ..or issues with no labels
|
|
|
|
await connection.onMergeRequestCommentCreated(GITLAB_MR_COMMENT);
|
|
|
|
await waitForDebouncing();
|
|
|
|
intent.expectNoEvent();
|
|
|
|
});
|
|
|
|
|
2025-02-25 13:23:33 +00:00
|
|
|
it("will filter out issues matching excludingLabels.", async () => {
|
2025-02-25 12:12:49 +00:00
|
|
|
const { connection, intent } = createConnection({
|
|
|
|
excludingLabels: ["exclude-me"]
|
|
|
|
});
|
|
|
|
// ..or issues with no labels
|
|
|
|
await connection.onMergeRequestCommentCreated({
|
|
|
|
...GITLAB_MR_COMMENT,
|
|
|
|
merge_request: {
|
|
|
|
...GITLAB_MR,
|
|
|
|
labels: [{
|
|
|
|
id: 0,
|
|
|
|
title: 'exclude-me'
|
|
|
|
} as any]
|
|
|
|
}
|
|
|
|
});
|
|
|
|
await waitForDebouncing();
|
|
|
|
intent.expectNoEvent();
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-08-14 14:58:21 +02:00
|
|
|
it("will debounce MR comments", async () => {
|
|
|
|
const { connection, intent } = createConnection();
|
2025-02-25 12:12:49 +00:00
|
|
|
await connection.onMergeRequestCommentCreated(GITLAB_MR_COMMENT);
|
|
|
|
await connection.onMergeRequestCommentCreated({
|
2023-08-14 14:58:21 +02:00
|
|
|
...GITLAB_MR_COMMENT,
|
|
|
|
'object_attributes': {
|
|
|
|
...GITLAB_MR_COMMENT.object_attributes,
|
|
|
|
'discussion_id': 'fa5d',
|
|
|
|
'note': 'different comment',
|
|
|
|
},
|
|
|
|
} as never);
|
|
|
|
await waitForDebouncing();
|
|
|
|
expect(intent.sentEvents.length).to.equal(1);
|
|
|
|
intent.expectEventMatches(
|
|
|
|
(ev: any) => ev.content.body.includes('with 2 comments'),
|
|
|
|
'one event sent for both comments',
|
|
|
|
0,
|
|
|
|
);
|
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2023-08-14 14:58:21 +02:00
|
|
|
it("will add new comments in a Matrix thread", async () => {
|
|
|
|
const { connection, intent } = createConnection();
|
2025-02-25 12:12:49 +00:00
|
|
|
await connection.onMergeRequestCommentCreated(GITLAB_MR_COMMENT);
|
2023-08-14 14:58:21 +02:00
|
|
|
await waitForDebouncing();
|
2025-02-25 12:12:49 +00:00
|
|
|
await connection.onMergeRequestCommentCreated(GITLAB_MR_COMMENT);
|
2023-08-14 14:58:21 +02:00
|
|
|
await waitForDebouncing();
|
|
|
|
expect(intent.sentEvents.length).to.equal(2);
|
|
|
|
intent.expectEventMatches(
|
|
|
|
(ev: any) => ev.content['m.relates_to'].event_id === 'event_0',
|
|
|
|
'one event sent for both comments',
|
|
|
|
1,
|
|
|
|
);
|
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2023-08-14 14:58:21 +02:00
|
|
|
it("will correctly map new comments to aggregated discussions", async () => {
|
|
|
|
const { connection, intent } = createConnection();
|
2025-02-25 12:12:49 +00:00
|
|
|
await connection.onMergeRequestCommentCreated({
|
2023-08-14 14:58:21 +02:00
|
|
|
...GITLAB_MR_COMMENT,
|
|
|
|
'object_attributes': {
|
|
|
|
...GITLAB_MR_COMMENT.object_attributes,
|
|
|
|
'discussion_id': 'disc1',
|
|
|
|
},
|
|
|
|
} as never);
|
2025-02-25 12:12:49 +00:00
|
|
|
await connection.onMergeRequestCommentCreated({
|
2023-08-14 14:58:21 +02:00
|
|
|
...GITLAB_MR_COMMENT,
|
|
|
|
'object_attributes': {
|
|
|
|
...GITLAB_MR_COMMENT.object_attributes,
|
|
|
|
'discussion_id': 'disc2',
|
|
|
|
},
|
|
|
|
} as never);
|
|
|
|
await waitForDebouncing();
|
|
|
|
expect(intent.sentEvents.length).to.equal(1);
|
|
|
|
|
2025-02-25 12:12:49 +00:00
|
|
|
await connection.onMergeRequestCommentCreated({
|
2023-08-14 14:58:21 +02:00
|
|
|
...GITLAB_MR_COMMENT,
|
|
|
|
'object_attributes': {
|
|
|
|
...GITLAB_MR_COMMENT.object_attributes,
|
|
|
|
'discussion_id': 'disc1',
|
|
|
|
},
|
|
|
|
} as never);
|
|
|
|
await waitForDebouncing();
|
|
|
|
expect(intent.sentEvents.length).to.equal(2);
|
|
|
|
intent.expectEventMatches(
|
|
|
|
(ev: any) => ev.content['m.relates_to'].event_id === 'event_0',
|
|
|
|
'disc1 reply goes to existing thread',
|
|
|
|
1
|
|
|
|
);
|
|
|
|
|
2025-02-25 12:12:49 +00:00
|
|
|
await connection.onMergeRequestCommentCreated({
|
2023-08-14 14:58:21 +02:00
|
|
|
...GITLAB_MR_COMMENT,
|
|
|
|
'object_attributes': {
|
|
|
|
...GITLAB_MR_COMMENT.object_attributes,
|
|
|
|
'discussion_id': 'disc2',
|
|
|
|
},
|
|
|
|
} as never);
|
|
|
|
await waitForDebouncing();
|
|
|
|
expect(intent.sentEvents.length).to.equal(3);
|
|
|
|
intent.expectEventMatches(
|
|
|
|
(ev: any) => ev.content['m.relates_to'].event_id === 'event_0',
|
|
|
|
'disc2 reply also goes to existing thread',
|
|
|
|
2
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2022-05-12 11:28:55 +01:00
|
|
|
describe("onIssueCreated", () => {
|
|
|
|
it("will handle a simple issue", async () => {
|
2023-01-13 10:32:09 -05:00
|
|
|
const { connection, intent } = createConnection();
|
2022-05-12 11:28:55 +01:00
|
|
|
await connection.onMergeRequestOpened(GITLAB_ISSUE_CREATED_PAYLOAD as never);
|
|
|
|
// Statement text.
|
2023-01-13 10:32:09 -05:00
|
|
|
intent.expectEventBodyContains('**alice** opened a new MR', 0);
|
|
|
|
intent.expectEventBodyContains(GITLAB_ISSUE_CREATED_PAYLOAD.object_attributes.url, 0);
|
|
|
|
intent.expectEventBodyContains(GITLAB_ISSUE_CREATED_PAYLOAD.object_attributes.title, 0);
|
2022-05-12 11:28:55 +01:00
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2022-05-12 11:28:55 +01:00
|
|
|
it("will filter out issues not matching includingLabels.", async () => {
|
2023-01-13 10:32:09 -05:00
|
|
|
const { connection, intent } = createConnection({
|
2022-05-12 11:28:55 +01:00
|
|
|
includingLabels: ["include-me"]
|
|
|
|
});
|
|
|
|
await connection.onMergeRequestOpened({
|
|
|
|
...GITLAB_ISSUE_CREATED_PAYLOAD,
|
|
|
|
labels: [{
|
|
|
|
title: "foo",
|
|
|
|
}],
|
|
|
|
} as never);
|
|
|
|
// ..or issues with no labels
|
|
|
|
await connection.onMergeRequestOpened(GITLAB_ISSUE_CREATED_PAYLOAD as never);
|
2023-01-13 10:32:09 -05:00
|
|
|
intent.expectNoEvent();
|
2022-05-12 11:28:55 +01:00
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2022-05-12 11:28:55 +01:00
|
|
|
it("will filter out issues matching excludingLabels.", async () => {
|
2023-01-13 10:32:09 -05:00
|
|
|
const { connection, intent } = createConnection({
|
2022-05-12 11:28:55 +01:00
|
|
|
excludingLabels: ["exclude-me"]
|
|
|
|
});
|
|
|
|
await connection.onMergeRequestOpened({
|
|
|
|
...GITLAB_ISSUE_CREATED_PAYLOAD,
|
|
|
|
labels: [{
|
|
|
|
title: "exclude-me",
|
|
|
|
}],
|
|
|
|
} as never);
|
2023-01-13 10:32:09 -05:00
|
|
|
intent.expectNoEvent();
|
2022-05-12 11:28:55 +01:00
|
|
|
});
|
2024-11-28 15:04:01 +00:00
|
|
|
|
2022-05-12 11:28:55 +01:00
|
|
|
it("will include issues matching includingLabels.", async () => {
|
2023-01-13 10:32:09 -05:00
|
|
|
const { connection, intent } = createConnection({
|
2025-02-25 12:12:49 +00:00
|
|
|
includingLabels: ["include-me"]
|
2022-05-12 11:28:55 +01:00
|
|
|
});
|
|
|
|
await connection.onMergeRequestOpened({
|
|
|
|
...GITLAB_ISSUE_CREATED_PAYLOAD,
|
|
|
|
labels: [{
|
|
|
|
title: "include-me",
|
|
|
|
}],
|
|
|
|
} as never);
|
2023-01-13 10:32:09 -05:00
|
|
|
intent.expectEventBodyContains('**alice** opened a new MR', 0);
|
2022-05-12 11:28:55 +01:00
|
|
|
});
|
|
|
|
});
|
2023-01-13 10:32:09 -05:00
|
|
|
});
|