diff --git a/src/app.js b/src/app.js index d537fef..f6ed22f 100644 --- a/src/app.js +++ b/src/app.js @@ -18,5 +18,8 @@ export default async (app, { getRouter }) => { app.on("issues.opened", issue_opened); // Comment is created in an issue by a user - app.on("issue_comment.created", issue_comment_created); + app.on( + ["issue_comment.created", "issue_comment.edited"], + issue_comment_created + ); }; diff --git a/src/handlers/issue_comment_created.js b/src/handlers/issue_comment_created.js index 13f48af..1aaa0b5 100644 --- a/src/handlers/issue_comment_created.js +++ b/src/handlers/issue_comment_created.js @@ -12,16 +12,21 @@ export default async (context) => { const collaboratorsJson = await context.octokit.repos.listCollaborators( context.repo({}) ); - const collaborators = collaboratorsJson.data.map((coll) => coll.login); + const collaborators = collaboratorsJson.data + .filter((coll) => { + return ( + coll.permissions.admin || + coll.permissions.maintain || + coll.permissions.triage + ); + }) + .map((coll) => coll.login); if (skipCommenters(commenter, collaborators)) return; const config = await getConfig(context); - const assignPromptKey = "assign-prompt"; - const unassignPromptKey = "unassign-prompt"; const assignees = issue.assignees.map((assigneeJson) => assigneeJson.login); - const assigneeCount = assignees.length; const request = checkRequest(comment.body, config); if (request === Request.SKIP) return; diff --git a/src/handlers/issue_opened.js b/src/handlers/issue_opened.js index 5c7d26c..ce6211d 100644 --- a/src/handlers/issue_opened.js +++ b/src/handlers/issue_opened.js @@ -6,7 +6,15 @@ export default async (context) => { const collaboratorsJson = await context.octokit.repos.listCollaborators( context.repo({}) ); - const collaborators = collaboratorsJson.data.map((coll) => coll.login); + const collaborators = collaboratorsJson.data + .filter((coll) => { + return ( + coll.permissions.admin || + coll.permissions.maintain || + coll.permissions.triage + ); + }) + .map((coll) => coll.login); const issue_opener_username = issue.user.login; const config = await getConfig(context); @@ -18,7 +26,7 @@ export default async (context) => { ); if (issue_opener === OpenerIsMaintainer.SKIP) return; const issueComment = context.issue({ - body: `@${issue_opener} ` + config[issue_opener], + body: `@${issue_opener_username} ` + config[issue_opener], }); return await context.octokit.issues.createComment(issueComment); }; diff --git a/src/helpers/check_request.js b/src/helpers/check_request.js index a0bdc50..2d9511a 100644 --- a/src/helpers/check_request.js +++ b/src/helpers/check_request.js @@ -9,11 +9,11 @@ export function checkRequest(comment, config) { .replace(/\s+/g, " ") // trim whitespace .toLowerCase(); // Case insensitive - if (config[Request.ASSIGN]) { - if (comment.includes(config[Request.ASSIGN])) return Request.ASSIGN; - else return Request.SKIP; - } else if (config[Request.UNASSIGN]) { - if (comment.includes(config[Request.UNASSIGN])) return Request.UNASSIGN; - else return Request.SKIP; - } else return Request.SKIP; + if (config[Request.ASSIGN] && comment.includes(config[Request.ASSIGN])) { + return Request.ASSIGN; + } + if (config[Request.UNASSIGN] && comment.includes(config[Request.UNASSIGN])) { + return Request.UNASSIGN; + } + return Request.SKIP; } diff --git a/src/helpers/get_assigned_issues.js b/src/helpers/get_assigned_issues.js index ef68c7c..52843a7 100644 --- a/src/helpers/get_assigned_issues.js +++ b/src/helpers/get_assigned_issues.js @@ -2,7 +2,7 @@ export default async function getAssignedIssues(context, username) { try { const response = await context.octokit.issues.listForRepo( - context.issue({ + context.repo({ assignee: username, state: "open", }) diff --git a/src/server.js b/src/server.js index 9081762..35ba688 100644 --- a/src/server.js +++ b/src/server.js @@ -1,3 +1,4 @@ +import { run } from "probot"; import app from "./app.js"; run(app); diff --git a/test/fixtures/context.txt b/test/fixtures/context.txt deleted file mode 100644 index 918457a..0000000 --- a/test/fixtures/context.txt +++ /dev/null @@ -1,258 +0,0 @@ -Context { - name: 'issues', - id: 'dfb093d0-276b-11ef-8145-a2fb608ec5b9', - payload: { - action: 'opened', - issue: { - url: 'https://api.github.com/repos/Varun-Kolanu/test/issues/36', - repository_url: 'https://api.github.com/repos/Varun-Kolanu/test', - labels_url: 'https://api.github.com/repos/Varun-Kolanu/test/issues/36/labels{/name}', - comments_url: 'https://api.github.com/repos/Varun-Kolanu/test/issues/36/comments', - events_url: 'https://api.github.com/repos/Varun-Kolanu/test/issues/36/events', - html_url: 'https://github.com/Varun-Kolanu/test/issues/36', - id: 2344811747, - node_id: 'I_kwDOME3NOM6Lwvzj', - number: 36, - title: 'a', - user: [Object], - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 0, - created_at: '2024-06-10T20:56:13Z', - updated_at: '2024-06-10T20:56:13Z', - closed_at: null, - author_association: 'OWNER', - active_lock_reason: null, - body: 'a', - reactions: [Object], - timeline_url: 'https://api.github.com/repos/Varun-Kolanu/test/issues/36/timeline', - performed_via_github_app: null, - state_reason: null - }, - repository: { - id: 810405176, - node_id: 'R_kgDOME3NOA', - name: 'test', - full_name: 'Varun-Kolanu/test', - private: true, - owner: [Object], - html_url: 'https://github.com/Varun-Kolanu/test', - description: null, - fork: false, - url: 'https://api.github.com/repos/Varun-Kolanu/test', - forks_url: 'https://api.github.com/repos/Varun-Kolanu/test/forks', - keys_url: 'https://api.github.com/repos/Varun-Kolanu/test/keys{/key_id}', - collaborators_url: 'https://api.github.com/repos/Varun-Kolanu/test/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/Varun-Kolanu/test/teams', - hooks_url: 'https://api.github.com/repos/Varun-Kolanu/test/hooks', - issue_events_url: 'https://api.github.com/repos/Varun-Kolanu/test/issues/events{/number}', - events_url: 'https://api.github.com/repos/Varun-Kolanu/test/events', - assignees_url: 'https://api.github.com/repos/Varun-Kolanu/test/assignees{/user}', - branches_url: 'https://api.github.com/repos/Varun-Kolanu/test/branches{/branch}', - tags_url: 'https://api.github.com/repos/Varun-Kolanu/test/tags', - blobs_url: 'https://api.github.com/repos/Varun-Kolanu/test/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/Varun-Kolanu/test/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/Varun-Kolanu/test/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/Varun-Kolanu/test/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/Varun-Kolanu/test/statuses/{sha}', - languages_url: 'https://api.github.com/repos/Varun-Kolanu/test/languages', - stargazers_url: 'https://api.github.com/repos/Varun-Kolanu/test/stargazers', - contributors_url: 'https://api.github.com/repos/Varun-Kolanu/test/contributors', - subscribers_url: 'https://api.github.com/repos/Varun-Kolanu/test/subscribers', - subscription_url: 'https://api.github.com/repos/Varun-Kolanu/test/subscription', - commits_url: 'https://api.github.com/repos/Varun-Kolanu/test/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/Varun-Kolanu/test/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/Varun-Kolanu/test/comments{/number}', - issue_comment_url: 'https://api.github.com/repos/Varun-Kolanu/test/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/Varun-Kolanu/test/contents/{+path}', - compare_url: 'https://api.github.com/repos/Varun-Kolanu/test/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/Varun-Kolanu/test/merges', - archive_url: 'https://api.github.com/repos/Varun-Kolanu/test/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/Varun-Kolanu/test/downloads', - issues_url: 'https://api.github.com/repos/Varun-Kolanu/test/issues{/number}', - pulls_url: 'https://api.github.com/repos/Varun-Kolanu/test/pulls{/number}', - milestones_url: 'https://api.github.com/repos/Varun-Kolanu/test/milestones{/number}', - notifications_url: 'https://api.github.com/repos/Varun-Kolanu/test/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/Varun-Kolanu/test/labels{/name}', - releases_url: 'https://api.github.com/repos/Varun-Kolanu/test/releases{/id}', - deployments_url: 'https://api.github.com/repos/Varun-Kolanu/test/deployments', - created_at: '2024-06-04T16:22:19Z', - updated_at: '2024-06-08T20:00:26Z', - pushed_at: '2024-06-08T19:27:57Z', - git_url: 'git://github.com/Varun-Kolanu/test.git', - ssh_url: 'git@github.com:Varun-Kolanu/test.git', - clone_url: 'https://github.com/Varun-Kolanu/test.git', - svn_url: 'https://github.com/Varun-Kolanu/test', - homepage: null, - size: 2, - stargazers_count: 0, - watchers_count: 0, - language: null, - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: false, - has_pages: false, - has_discussions: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 30, - license: null, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 30, - watchers: 0, - default_branch: 'main' - }, - sender: { - login: 'Varun-Kolanu', - id: 112728411, - node_id: 'U_kgDOBrgZWw', - avatar_url: 'https://avatars.githubusercontent.com/u/112728411?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/Varun-Kolanu', - html_url: 'https://github.com/Varun-Kolanu', - followers_url: 'https://api.github.com/users/Varun-Kolanu/followers', - following_url: 'https://api.github.com/users/Varun-Kolanu/following{/other_user}', - gists_url: 'https://api.github.com/users/Varun-Kolanu/gists{/gist_id}', - starred_url: 'https://api.github.com/users/Varun-Kolanu/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/Varun-Kolanu/subscriptions', - organizations_url: 'https://api.github.com/users/Varun-Kolanu/orgs', - repos_url: 'https://api.github.com/users/Varun-Kolanu/repos', - events_url: 'https://api.github.com/users/Varun-Kolanu/events{/privacy}', - received_events_url: 'https://api.github.com/users/Varun-Kolanu/received_events', - type: 'User', - site_admin: false - }, - installation: { - id: 51718023, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNTE3MTgwMjM=' - } - }, - octokit: OctokitWithDefaults { - request: [Function: newApi] { - endpoint: [Function], - defaults: [Function: bound withDefaults] - }, - graphql: [Function: newApi] { - defaults: [Function: bound withDefaults], - endpoint: [Function] - }, - log: { - debug: [Function: bound noop], - info: [Function: bound LOG], - warn: [Function: bound LOG], - error: [Function: bound LOG] - }, - hook: [Function: bound register] { - api: [Object], - remove: [Function: bound removeHook], - before: [Function: bound addHook], - error: [Function: bound addHook], - after: [Function: bound addHook], - wrap: [Function: bound addHook] - }, - auth: [Function: bound auth] AsyncFunction { - hook: [Function: bound hook] AsyncFunction - }, - retry: { retryRequest: [Function: retryRequest] }, - paginate: [Function: bound paginate] { iterator: [Function: bound iterator] }, - actions: { octokit: [Circular *1], scope: 'actions', cache: {} }, - activity: { octokit: [Circular *1], scope: 'activity', cache: {} }, - apps: { octokit: [Circular *1], scope: 'apps', cache: {} }, - billing: { octokit: [Circular *1], scope: 'billing', cache: {} }, - checks: { octokit: [Circular *1], scope: 'checks', cache: {} }, - codeScanning: { octokit: [Circular *1], scope: 'codeScanning', cache: {} }, - codesOfConduct: { octokit: [Circular *1], scope: 'codesOfConduct', cache: {} }, - codespaces: { octokit: [Circular *1], scope: 'codespaces', cache: {} }, - copilot: { octokit: [Circular *1], scope: 'copilot', cache: {} }, - dependabot: { octokit: [Circular *1], scope: 'dependabot', cache: {} }, - dependencyGraph: { octokit: [Circular *1], scope: 'dependencyGraph', cache: {} }, - emojis: { octokit: [Circular *1], scope: 'emojis', cache: {} }, - gists: { octokit: [Circular *1], scope: 'gists', cache: {} }, - git: { octokit: [Circular *1], scope: 'git', cache: {} }, - gitignore: { octokit: [Circular *1], scope: 'gitignore', cache: {} }, - interactions: { octokit: [Circular *1], scope: 'interactions', cache: {} }, - issues: { octokit: [Circular *1], scope: 'issues', cache: {} }, - licenses: { octokit: [Circular *1], scope: 'licenses', cache: {} }, - markdown: { octokit: [Circular *1], scope: 'markdown', cache: {} }, - meta: { octokit: [Circular *1], scope: 'meta', cache: {} }, - migrations: { octokit: [Circular *1], scope: 'migrations', cache: {} }, - oidc: { octokit: [Circular *1], scope: 'oidc', cache: {} }, - orgs: { octokit: [Circular *1], scope: 'orgs', cache: {} }, - packages: { octokit: [Circular *1], scope: 'packages', cache: {} }, - projects: { octokit: [Circular *1], scope: 'projects', cache: {} }, - pulls: { octokit: [Circular *1], scope: 'pulls', cache: {} }, - rateLimit: { octokit: [Circular *1], scope: 'rateLimit', cache: {} }, - reactions: { octokit: [Circular *1], scope: 'reactions', cache: {} }, - repos: { octokit: [Circular *1], scope: 'repos', cache: [Object] }, - search: { octokit: [Circular *1], scope: 'search', cache: {} }, - secretScanning: { octokit: [Circular *1], scope: 'secretScanning', cache: {} }, - securityAdvisories: { octokit: [Circular *1], scope: 'securityAdvisories', cache: {} }, - teams: { octokit: [Circular *1], scope: 'teams', cache: {} }, - users: { octokit: [Circular *1], scope: 'users', cache: {} }, - rest: { - actions: [Object], - activity: [Object], - apps: [Object], - billing: [Object], - checks: [Object], - codeScanning: [Object], - codesOfConduct: [Object], - codespaces: [Object], - copilot: [Object], - dependabot: [Object], - dependencyGraph: [Object], - emojis: [Object], - gists: [Object], - git: [Object], - gitignore: [Object], - interactions: [Object], - issues: [Object], - licenses: [Object], - markdown: [Object], - meta: [Object], - migrations: [Object], - oidc: [Object], - orgs: [Object], - packages: [Object], - projects: [Object], - pulls: [Object], - rateLimit: [Object], - reactions: [Object], - repos: [Object], - search: [Object], - secretScanning: [Object], - securityAdvisories: [Object], - teams: [Object], - users: [Object] - }, - config: { get: [AsyncFunction: get] } - }, - log: EventEmitter { - trace: [Function: noop], - debug: [Function: noop], - info: [Function: LOG], - warn: [Function: LOG], - error: [Function: LOG], - fatal: [Function (anonymous)], - [Symbol(pino.serializers)]: { err: [Function: errSerializer] }, - [Symbol(pino.formatters)]: { - level: [Function: level], - bindings: [Function: resetChildingsFormatter], - log: undefined - }, - [Symbol(pino.chindings)]: ',"pid":14068,"hostname":"VARUN","name":"probot","name":"event","id":"dfb093d0-276b-11ef-8145-a2fb608ec5b9"', - [Symbol(pino.levelVal)]: 30 - } -} \ No newline at end of file diff --git a/test/fixtures/issue_comment.created.json b/test/fixtures/issue_comment.created.json new file mode 100644 index 0000000..9aadcae --- /dev/null +++ b/test/fixtures/issue_comment.created.json @@ -0,0 +1,20 @@ +{ + "action": "created", + "issue": { + "number": 1 + }, + "repository": { + "name": "test-repo", + "owner": { + "login": "test-owner" + } + }, + "comment": { + "user": { + "login": "test" + } + }, + "installation": { + "id": 2 + } +} diff --git a/test/fixtures/issues.opened.json b/test/fixtures/issues.opened.json index b1f1ec9..0d2eabe 100644 --- a/test/fixtures/issues.opened.json +++ b/test/fixtures/issues.opened.json @@ -1,10 +1,7 @@ { "action": "opened", "issue": { - "number": 1, - "user": { - "login": "test-opener" - } + "number": 1 }, "repository": { "name": "test-repo", diff --git a/test/index.test.js b/test/index.test.js deleted file mode 100644 index 102dd73..0000000 --- a/test/index.test.js +++ /dev/null @@ -1,62 +0,0 @@ -import nock from "nock"; -import payload from "./fixtures/issues.opened.json" with { type: "json" }; -// import config from "./fixtures/sample-config.json" with { type: "json" }; -import { describe, beforeEach, afterEach, test } from "node:test"; -import assert from "node:assert"; - -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import getProbotConfig from "./utils/get-probot.js"; - - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const configYml = fs.readFileSync( - path.join(__dirname, "fixtures/sample-config.yml"), -); -const YmlStringified = configYml.toString("utf-8"); - -describe("Issue Assigner App", () => { - let probot; - let server; - - beforeEach(async () => { - nock.disableNetConnect(); - server = await getProbotConfig(); - probot = server.probotApp; - }); - - test("creates a comment when an issue is opened", async () => { - const mock = nock("https://api.github.com") - - // Test that access token is requested - .post("/app/installations/2/access_tokens") - .reply(200, { - token: "test" - }) - - // Test that collaborators are fetched - .get("/repos/test-owner/test-repo/collaborators") - .reply(200, - [ - { - "login": "test-owner", - } - ] - ) - - // Test that config is loaded - .get("/repos/test-owner/test-repo/contents/.github\%2Fissue-assigner.yml") - .reply(200, YmlStringified) - - // Receive a webhook event - await probot.receive({ name: "issues", payload }); - - }); - - afterEach(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); -}); diff --git a/test/integration/issue_comment_created.test.js b/test/integration/issue_comment_created.test.js new file mode 100644 index 0000000..2cdf2d8 --- /dev/null +++ b/test/integration/issue_comment_created.test.js @@ -0,0 +1,174 @@ +import nock from "nock"; +import { describe, beforeEach, afterEach, test } from "node:test"; +import assert from "node:assert"; + +import getProbotConfig from "../utils/get-probot.js"; +import { setupNock, setupNockSkip } from "../utils/setupNocks.js"; +import { createPayloadIssueComment } from "../utils/createPayload.js"; + +describe("Issue Comment Created handler", () => { + let probot; + let server; + let mock; + + beforeEach(async () => { + nock.disableNetConnect(); + server = await getProbotConfig(); + probot = server.probotApp; + }); + + test("only fetches until collaborators and returns if commenter is a bot", async () => { + mock = setupNockSkip(); + + await probot.receive({ + name: "issue_comment", + payload: createPayloadIssueComment("issue-assigner[bot]"), + }); + }); + + test("only fetches until collaborators and returns if commenter is a maintainer", async () => { + mock = setupNockSkip(); + + await probot.receive({ + name: "issue_comment", + payload: createPayloadIssueComment("test-maintainer"), + }); + }); + + test("assigns an issue if requested to assign and all OK!", async () => { + mock = setupNock( + "@test-commenter This issue has been successfully assigned to you! 🚀" + ) + // Fetch already assigned issues + .get( + "/repos/test-owner/test-repo/issues?assignee=test-commenter&state=open" + ) + .reply(200, []) + + // Assign issue + .post("/repos/test-owner/test-repo/issues/1/assignees", (body) => { + assert.deepStrictEqual(body, { + assignees: ["test-commenter"], + }); + return true; + }) + .reply(200); + + await probot.receive({ + name: "issue_comment", + payload: createPayloadIssueComment( + "test-commenter", + "@issue-assigner claim" + ), + }); + }); + + describe("Doesn't assign the issue if requested to assign and if ", () => { + test("that issue is already assigned to the commeneter", async () => { + mock = setupNock( + "@assignee1 You have already been assigned to this issue." + ) + // Fetch already assigned issues + .get("/repos/test-owner/test-repo/issues?assignee=assignee1&state=open") + .reply(200, [ + { + number: 1, + html_url: "sampleurl", + }, + ]); + + await probot.receive({ + name: "issue_comment", + payload: createPayloadIssueComment( + "assignee1", + "@issue-assigner claim", + true + ), + }); + }); + + test("max assignees reached in issue", async () => { + mock = setupNock( + "@test-commenter Sorry, maximum limit for assignees in this issue has reached. Please check other issues or contact a maintainer." + ) + // Fetch already assigned issues + .get( + "/repos/test-owner/test-repo/issues?assignee=test-commenter&state=open" + ) + .reply(200, []); + + await probot.receive({ + name: "issue_comment", + payload: createPayloadIssueComment( + "test-commenter", + "@issue-assigner claim", + true + ), + }); + }); + + test("max issues for user reached in repo", async () => { + mock = setupNock( + "@test-commenter You already have this issue assigned: [ issue#2 ](sample-url). Abandon your existing issue or contact a maintainer if you want to get this issue assigned." + ) + // Fetch already assigned issues + .get( + "/repos/test-owner/test-repo/issues?assignee=test-commenter&state=open" + ) + .reply(200, [ + { + number: 2, + html_url: "sample-url", + }, + ]); + + await probot.receive({ + name: "issue_comment", + payload: createPayloadIssueComment( + "test-commenter", + "@issue-assigner claim" + ), + }); + }); + }); + + test("unassigns an issue if requested to unassign and all OK!", async () => { + mock = setupNock( + "@assignee1 You have been unassigned to this issue successfully." + ) + // Unassign the issue + .delete("/repos/test-owner/test-repo/issues/1/assignees", (body) => { + assert.deepStrictEqual(body, { assignees: ["assignee1"] }); + return true; + }) + .reply(201); + + await probot.receive({ + name: "issue_comment", + payload: createPayloadIssueComment( + "assignee1", + "@issue-assigner abandon", + true + ), + }); + }); + + test("doesn't unassign an issue if requested to unassign and already not assigned", async () => { + mock = setupNock("@test-commenter You were not assigned to this issue."); + + await probot.receive({ + name: "issue_comment", + payload: createPayloadIssueComment( + "test-commenter", + "@issue-assigner abandon", + true + ), + }); + }); + + afterEach(() => { + assert.deepStrictEqual(mock.activeMocks(), []); + nock.cleanAll(); + nock.enableNetConnect(); + }); +}); diff --git a/test/integration/issue_opened.test.js b/test/integration/issue_opened.test.js new file mode 100644 index 0000000..e393b28 --- /dev/null +++ b/test/integration/issue_opened.test.js @@ -0,0 +1,48 @@ +import nock from "nock"; +import { describe, beforeEach, afterEach, test } from "node:test"; +import assert from "node:assert"; + +import getProbotConfig from "../utils/get-probot.js"; +import { setupNock } from "../utils/setupNocks.js"; +import { createPayloadIssueOpened } from "../utils/createPayload.js"; + +describe("Issue Opened handler", () => { + let probot; + let server; + let mock; + + beforeEach(async () => { + nock.disableNetConnect(); + server = await getProbotConfig(); + probot = server.probotApp; + }); + + test("comments on an issue when opened by a maintainer", async () => { + mock = setupNock( + "@test-maintainer Comment '@issue-assigner claim' to get this issue assigned or '@issue-assigner abandon' to get this issue unassigned." + ); + + await probot.receive({ + name: "issues", + payload: createPayloadIssueOpened("test-maintainer"), + }); + assert.deepStrictEqual(mock.activeMocks(), []); + }); + + test("comments on an issue when opened by a non maintainer", async () => { + mock = setupNock( + "@test-opener Thank you for opening this issue. Maintainers will check and approve if seems to be useful." + ); + + await probot.receive({ + name: "issues", + payload: createPayloadIssueOpened("test-opener"), + }); + }); + + afterEach(() => { + assert.deepStrictEqual(mock.activeMocks(), []); + nock.cleanAll(); + nock.enableNetConnect(); + }); +}); diff --git a/test/unit/commenters.test.js b/test/unit/commenters.test.js index ef6cc52..fd88d4b 100644 --- a/test/unit/commenters.test.js +++ b/test/unit/commenters.test.js @@ -2,24 +2,21 @@ import { describe, test } from "node:test"; import assert from "node:assert"; import { skipCommenters } from "../../src/helpers/skip_commenters.js"; -describe("If Commenter", () => { - let collaborators = ["test-collaborator"]; - test("is a bot, skip", () => { +describe("Skip Commenters", () => { + let maintainers = ["test-maintainer"]; + test("if commenter is a bot", () => { ["issue-assigner[bot]", "test [bot]"].forEach((username) => { - assert.strictEqual(skipCommenters(username, collaborators), true); + assert.strictEqual(skipCommenters(username, maintainers), true); }); - assert.strictEqual(skipCommenters("normal-user", collaborators), false); + assert.strictEqual(skipCommenters("normal-user", maintainers), false); }); - test("is a collaborator, skip", () => { - assert.strictEqual( - skipCommenters("test-collaborator", collaborators), - true - ); + test("if commenter is a maintainer", () => { + assert.strictEqual(skipCommenters("test-maintainer", maintainers), true); assert.strictEqual( - skipCommenters("test-not-collaborator", collaborators), + skipCommenters("test-not-maintainer", maintainers), false ); }); diff --git a/test/unit/config.test.js b/test/unit/config.test.js index 5746af1..b11b679 100644 --- a/test/unit/config.test.js +++ b/test/unit/config.test.js @@ -3,9 +3,9 @@ import assert from "node:assert"; import { checkConfig } from "../../src/helpers/check_config.js"; import replacePlaceholders from "../../src/helpers/replace_name.js"; -describe("If Config", () => { +describe("Configuration Validation and Placeholder Replacement", () => { let config; - test("is empty, throws error", () => { + test("throws error if config file is empty", () => { config = {}; assert.throws( () => { @@ -15,7 +15,7 @@ describe("If Config", () => { ); }); - test("doesn't contain name key, throws error", () => { + test("throws error if config file doesn't contain name key", () => { config = { "some-key": "some-value" }; assert.throws( () => { @@ -25,21 +25,21 @@ describe("If Config", () => { ); }); - test("has {name} in string, it is replaced", () => { + test("replaces {name} placeholder in string values", () => { config = { some_key: "{name} some-comment" }; assert.deepEqual(replacePlaceholders(config, "test-name"), { some_key: "test-name some-comment", }); }); - test("has {name} in array, it is replaced", () => { + test("replaces {name} placeholder in array values", () => { config = { some_key: ["{name} test"] }; assert.deepEqual(replacePlaceholders(config, "test-name"), { some_key: ["test-name test"], }); }); - test("has {name} in object, it is replaced", () => { + test("replaces {name} placeholder in object values", () => { config = { some_key: { some_deep: "{name} test" } }; assert.deepEqual(replacePlaceholders(config, "test-name"), { some_key: { some_deep: "test-name test" }, diff --git a/test/unit/opener_role.test.js b/test/unit/opener_role.test.js index 6598518..9ce183d 100644 --- a/test/unit/opener_role.test.js +++ b/test/unit/opener_role.test.js @@ -5,51 +5,51 @@ import { issueOpener, } from "../../src/helpers/issue_opener.js"; -describe("If Issue Opener", () => { - const collaborators = ["test-collaborator"]; +describe("Issue Opener handler", () => { + const maintainers = ["test-maintainer"]; let opener; let config; - test("is collaborator and comment for maintainer opened issues enabled", () => { - opener = "test-collaborator"; + test("returns maintainer if opener is a maintainer and config has comment for maintainer opened issues", () => { + opener = "test-maintainer"; config = { "issue-opener-is-maintainer": "test comment", }; assert.strictEqual( - issueOpener(config, collaborators, opener), + issueOpener(config, maintainers, opener), OpenerIsMaintainer.YES ); }); - test("is collaborator and comment for maintainer opened issues not enabled", () => { - opener = "test-collaborator"; + test("returns Skip if opener is a maintainer and config doesn't have comment for maintainer opened issues", () => { + opener = "test-maintainer"; config = {}; assert.strictEqual( - issueOpener(config, collaborators, opener), + issueOpener(config, maintainers, opener), OpenerIsMaintainer.SKIP ); }); - test("is not collaborator and comment for non-maintainer opened issues enabled", () => { - opener = "test-not-collaborator"; + test("returns Not maintainer if opener is not a maintainer and config has comment for non-maintainer opened issues", () => { + opener = "test-not-maintainer"; config = { "issue-opener-not-maintainer": "test comment", }; assert.strictEqual( - issueOpener(config, collaborators, opener), + issueOpener(config, maintainers, opener), OpenerIsMaintainer.NO ); }); - test("is not collaborator and comment for non-maintainer opened issues not enabled", () => { - opener = "test-not-collaborator"; + test("returns Skip if opener is not a maintainer and config doesn't have comment for non-maintainer opened issues", () => { + opener = "test-not-maintainer"; config = {}; assert.strictEqual( - issueOpener(config, collaborators, opener), + issueOpener(config, maintainers, opener), OpenerIsMaintainer.SKIP ); }); diff --git a/test/unit/request.test.js b/test/unit/request.test.js index 8153554..a0f88c2 100644 --- a/test/unit/request.test.js +++ b/test/unit/request.test.js @@ -2,11 +2,11 @@ import { describe, test } from "node:test"; import assert from "node:assert"; import { Request, checkRequest } from "../../src/helpers/check_request.js"; -describe("If Request", () => { +describe("Check Request Functionality", () => { let config1; let config2; let comments; - test("is to assign", () => { + test("identifies comments requesting assignment correctly", () => { config1 = { "assign-prompt": "claim", }; @@ -18,7 +18,7 @@ describe("If Request", () => { }); }); - test("is to unassign", () => { + test("identifies comments requesting unassignment correctly", () => { config1 = { "unassign-prompt": "abandon", }; @@ -30,7 +30,7 @@ describe("If Request", () => { }); }); - test("Nothing", () => { + test("skips comments not related to assignment or unassignment", () => { config1 = { "assign-prompt": "claim", "unassign-prompt": "abandon", diff --git a/test/unit/should_assign.test.js b/test/unit/should_assign.test.js index a415a28..86c92d8 100644 --- a/test/unit/should_assign.test.js +++ b/test/unit/should_assign.test.js @@ -2,7 +2,7 @@ import { describe, test, beforeEach } from "node:test"; import assert from "node:assert"; import { Assignment, shouldAssign } from "../../src/helpers/should_assign.js"; -describe("Requested Assignment", () => { +describe("Assignment Decision Logic", () => { let commenter; let assignees = ["assignee1"]; let config1; @@ -14,7 +14,7 @@ describe("Requested Assignment", () => { numAssignedIssues = 0; }); - test("issue already assigned", () => { + test("classifies correctly if issue is already assigned", () => { commenter = "assignee1"; config1 = { "issue-already-assigned": "test", @@ -29,7 +29,7 @@ describe("Requested Assignment", () => { ); }); - test("Max assignees reached", () => { + test("classifies correctly if Max assignees reached", () => { commenter = "assignee2"; config1 = { "max-assignees": 1, @@ -49,7 +49,7 @@ describe("Requested Assignment", () => { ); }); - test("Max issues reached", () => { + test("classifies correctly if Max issues reached", () => { commenter = "assignee2"; config1 = { "max-issues-for-user": 1, @@ -61,7 +61,7 @@ describe("Requested Assignment", () => { ); }); - test("Assign comment exists", () => { + test("classifies correctly assignment", () => { commenter = "assignee2"; assignees = []; config1 = { diff --git a/test/unit/should_unassign.test.js b/test/unit/should_unassign.test.js index ce6c1af..ddf29d0 100644 --- a/test/unit/should_unassign.test.js +++ b/test/unit/should_unassign.test.js @@ -5,12 +5,12 @@ import { shouldUnAssign, } from "../../src/helpers/should_unassign.js"; -describe("Requested Unassignment", () => { +describe("Unassignment Decision Logic", () => { let commenter; const assignees = ["assignee1"]; let config1; const config2 = {}; - test("Requester was already not assigned", () => { + test("classifies correctly if Requester was already not assigned", () => { commenter = "assignee2"; config1 = { "issue-was-not-assigned": "test", @@ -25,7 +25,7 @@ describe("Requested Unassignment", () => { ); }); - test("Requester was assigned", () => { + test("classifies correctly if Requester was assigned and says to unassign", () => { commenter = "assignee1"; config1 = { "unassigned-comment": "test", diff --git a/test/utils/createPayload.js b/test/utils/createPayload.js new file mode 100644 index 0000000..333b6d1 --- /dev/null +++ b/test/utils/createPayload.js @@ -0,0 +1,30 @@ +import payloadIssueOpened from "../fixtures/issues.opened.json" with { type: "json" }; +import payloadIssueComment from "../fixtures/issue_comment.created.json" with { type: "json" }; + +export const createPayloadIssueOpened = (login) => { + return { + ...payloadIssueOpened, + issue: { + ...payloadIssueOpened.issue, + user: { login }, + }, + }; +}; + +export const createPayloadIssueComment = (login, body, haveAssignee = false) => { + return { + ...payloadIssueComment, + issue: { + ...payloadIssueComment.issue, + assignees: haveAssignee ? [{ + "login": "assignee1" + }] : [] + }, + comment: { + user: { + login + }, + body + } + }; + }; diff --git a/test/utils/get_base64.js b/test/utils/get_base64.js deleted file mode 100644 index fe22a7f..0000000 --- a/test/utils/get_base64.js +++ /dev/null @@ -1,11 +0,0 @@ -function getBase64(file) { - var reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = function () { - return reader.result; - }; - reader.onerror = function (error) { - console.log("Error: ", error); - return null; - }; -} diff --git a/test/utils/setupNocks.js b/test/utils/setupNocks.js new file mode 100644 index 0000000..35122bb --- /dev/null +++ b/test/utils/setupNocks.js @@ -0,0 +1,38 @@ +import nock from "nock"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import assert from "node:assert"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const configYmlPath = path.join(__dirname, "../fixtures/sample-config.yml"); +const configYml = fs.readFileSync(configYmlPath, "utf-8"); + +export const setupNock = (commentBody) => { + return nock("https://api.github.com") + .post("/app/installations/2/access_tokens") + .reply(200, { token: "test" }) + .get("/repos/test-owner/test-repo/collaborators") + .reply(200, [{ login: "test-maintainer", permissions: { maintain: true } }]) + .get("/repos/test-owner/test-repo/contents/.github%2Fissue-assigner.yml") + .reply(200, configYml) + .post("/repos/test-owner/test-repo/issues/1/comments", (body) => { + assert.deepStrictEqual(body, { body: commentBody }); + return true; + }) + .reply(200); +}; + +export const setupNockSkip = () => { + return nock("https://api.github.com") + .post("/app/installations/2/access_tokens") + .reply(200, { token: "test" }) + .get("/repos/test-owner/test-repo/collaborators") + .reply(200, [ + { login: "test-maintainer", permissions: { maintain: true } }, + ]); +}; + +// export const setupNockAssigned = (commentBody) => { +// return setupNock(commentBody) +// };