UNPKG

@twilio-labs/languagetool-cli

Version:

Run LanguageTool for linting Markdown files during CI

322 lines 16.2 kB
import { suite } from "uvu"; import sinon from "sinon"; import expect from "expect"; import nock from "nock"; import { initializeOctokit, githubReporter, PR_COMMENT_COUNTER, } from "../lib/githubReporter.js"; import { MARKDOWN_ITEM_COUNTER } from "../lib/markdownReporter.js"; import { getJSONFixture, getFakeResult, MARKDOWN_RESPONSE, } from "./testUtilities.js"; const test = suite("githubReporter"); let f; const commentResponse = getJSONFixture("github_api/issue_comment_1.json"); const commentResponseTooBig = getJSONFixture("github_api/issue_comment_2.json"); const prResponse = getJSONFixture("github_api/get_pr_1.json"); const reviewResponse1 = getJSONFixture("github_api/review_comment_1.json"); const snoozeFunc = sinon.fake((ms) => { console.debug(`Fake snoozing for ${ms}ms...`); return new Promise((resolve) => setTimeout(resolve, 1)); }); let snoozeStub; test.before.each(async () => { process.env.GITHUB_TOKEN = "123456"; f = await getFakeResult(); f.fakeOptions.githubpr = "https://github.com/dprothero/testing-ground/pull/1"; nock("https://api.github.com:443", { encodedQueryParams: true }) .get("/repos/dprothero/testing-ground/pulls/1") .reply(200, prResponse.payload, prResponse.headers); snoozeStub = sinon.stub().callsFake(snoozeFunc); await initializeOctokit(f.fakeOptions.githubpr, snoozeStub); }); test("no github token", async () => { process.env.GITHUB_TOKEN = ""; let error; try { await initializeOctokit(""); } catch (err) { error = err.message; } expect(error).toEqual("No GITHUB_TOKEN environment variable specified. Cannot report to GitHub."); }); test("custom github enterprise host", async () => { nock("https://github.enterprise.com:443", { encodedQueryParams: true }) .get("/api/v3/repos/some-org/some-repo/pulls/1") .reply(200, prResponse.payload, prResponse.headers); await initializeOctokit("https://github.enterprise.com/some-org/some-repo/pull/1"); expect(nock.isDone()).toBeTruthy(); }); test("nothing to report on complete", async () => { expect(githubReporter.complete).toBeTruthy(); if (githubReporter.complete) await githubReporter.complete(f.fakeOptions, f.fakeStats); }); test("noIssue", async () => { const report = githubReporter.noIssues(f.fakeResult, f.fakeOptions, f.fakeStats); expect(report).toEqual(""); nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/issues/1/comments", { body: "<!-- languagetool-cli -->\n<details>\n<summary><h3>0 grammar issues found (click to expand)</h3></summary>\n\n- [X] **README.md** has no issues.\n\n\n</details>", }) .reply(201, commentResponse.payload, commentResponse.headers); if (githubReporter.complete) await githubReporter.complete(f.fakeOptions, f.fakeStats); expect(nock.isDone()).toBeTruthy(); }); test("issue with suggested line", async () => { nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", { path: "README.md", side: "RIGHT", line: 1, commit_id: prResponse.payload.head.sha, body: "<!-- languagetool-cli -->\nConsider a different word. `foo`\n\n```suggestion\nThe word is bar.\n```\n", }) .reply(201, reviewResponse1.payload, reviewResponse1.headers); await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(0); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(1); expect(nock.isDone()).toBeTruthy(); }); const notPartOfDiff = { message: "Validation Failed", errors: [ { resource: "PullRequestReviewComment", code: "custom", field: "pull_request_review_thread.line", message: "pull_request_review_thread.line must be part of the diff", }, { resource: "PullRequestReviewComment", code: "missing_field", field: "pull_request_review_thread.diff_hunk", }, ], documentation_url: "https://docs.github.com/rest", }; test("issue not part of diff", async () => { f.fakeItem.result.changedLines = []; await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(1); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(0); expect(nock.isDone()).toBeTruthy(); }); test("issue not part of diff, pr-diff-only", async () => { f.fakeItem.result.changedLines = []; f.fakeOptions["pr-diff-only"] = true; await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(0); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(0); expect(nock.isDone()).toBeTruthy(); }); test("issue not part of diff from api", async () => { nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", (postData) => postData.body.includes("```suggestion")) .reply(422, notPartOfDiff, reviewResponse1.headers); await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(1); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(0); expect(nock.isDone()).toBeTruthy(); }); test("issue not part of diff, pr-diff-only from api", async () => { nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", (postData) => postData.body.includes("```suggestion")) .reply(422, notPartOfDiff, reviewResponse1.headers); f.fakeOptions["pr-diff-only"] = true; await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(0); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(0); expect(nock.isDone()).toBeTruthy(); }); test("two issues, one suggestion, one general comment", async () => { nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", (postData) => postData.body.includes("```suggestion")) .reply(201, reviewResponse1.payload, reviewResponse1.headers); await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(0); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(1); nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", (postData) => postData.body.includes("```suggestion")) .reply(422, notPartOfDiff, reviewResponse1.headers); await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(1); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(1); nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/issues/1/comments", (postData) => postData.body.includes("1 additional")) .reply(201, commentResponse.payload, commentResponse.headers); if (githubReporter.complete) await githubReporter.complete(f.fakeOptions, f.fakeStats); expect(nock.isDone()).toBeTruthy(); }); test("two suggestions with rate limiting between calls", async () => { nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", (postData) => postData.body.includes("```suggestion")) .reply(201, reviewResponse1.payload, reviewResponse1.headers); await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(snoozeStub.called).toBeFalsy(); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(0); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(1); nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", (postData) => postData.body.includes("```suggestion")) .reply(201, reviewResponse1.payload, reviewResponse1.headers); await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(snoozeStub.called).toBeTruthy(); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(0); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(2); expect(nock.isDone()).toBeTruthy(); }); test("issue, some other github api error", async () => { nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", (postData) => postData.body.includes("```suggestion")) .reply(422, { message: "Validation Failed", errors: [ { resource: "PullRequestReviewComment", code: "custom", field: "pull_request_review_thread.line", message: "some other error", }, ], documentation_url: "https://docs.github.com/rest", }, reviewResponse1.headers); let error; try { await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); } catch (err) { error = err.message; } expect(error).toContain("some other error"); expect(nock.isDone()).toBeTruthy(); }); test("issue with suggested line, multiple suggestions", async () => { nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", (postData) => postData.body.includes("**Other suggestion(s):** `other`")) .reply(201, reviewResponse1.payload, reviewResponse1.headers); f.fakeItem.replacements.push("other"); await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(0); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(1); expect(nock.isDone()).toBeTruthy(); }); test("issue without suggested line", async () => { nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", { path: "README.md", side: "RIGHT", line: 1, commit_id: prResponse.payload.head.sha, body: "<!-- languagetool-cli -->\nConsider a different word. `foo`\n", }) .reply(201, reviewResponse1.payload, reviewResponse1.headers); f.fakeItem.suggestedLine = ""; await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(0); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(1); expect(nock.isDone()).toBeTruthy(); }); test("issue without suggested line, misspelling", async () => { nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/pulls/1/comments", { path: "README.md", side: "RIGHT", line: 1, commit_id: prResponse.payload.head.sha, body: "<!-- languagetool-cli -->\nConsider a different word. `foo`\n\nIf this is code (like a variable name), try surrounding it with \\`backticks\\`.", }) .reply(201, reviewResponse1.payload, reviewResponse1.headers); f.fakeItem.suggestedLine = ""; f.fakeItem.match.rule.issueType = "misspelling"; await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(0); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(1); expect(nock.isDone()).toBeTruthy(); }); test("issue hitting max suggestions", async () => { f.fakeOptions["max-pr-suggestions"] = 0; await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(1); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(0); nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/issues/1/comments", (postData) => postData.body.includes(MARKDOWN_RESPONSE)) .reply(201, commentResponse.payload, commentResponse.headers); if (githubReporter.complete) await githubReporter.complete(f.fakeOptions, f.fakeStats); expect(nock.isDone()).toBeTruthy(); }); test("all comments too large for github", async () => { const fakeItem2 = { ...f.fakeItem, contextHighlighted: "fee", currentLine: "The word is fee.", }; const fakeMarkdownItem1 = MARKDOWN_RESPONSE; const fakeMarkdownItem2 = MARKDOWN_RESPONSE.replace(/foo/g, "fee"); f.fakeOptions["max-pr-suggestions"] = 0; await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); await githubReporter.issue(fakeItem2, f.fakeOptions, f.fakeStats); expect(f.fakeStats.getCounter(MARKDOWN_ITEM_COUNTER)).toEqual(2); expect(f.fakeStats.getCounter(PR_COMMENT_COUNTER)).toEqual(0); // First request fails nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/issues/1/comments", (postData) => postData.body.includes(fakeMarkdownItem1) && postData.body.includes(fakeMarkdownItem2)) .reply(422, commentResponseTooBig.payload, commentResponseTooBig.headers); // Now we should get two separate requests, one for each comment nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/issues/1/comments", (postData) => postData.body.includes(fakeMarkdownItem1) && !postData.body.includes(fakeMarkdownItem2)) .reply(201, commentResponse.payload, commentResponse.headers); nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/issues/1/comments", (postData) => !postData.body.includes(fakeMarkdownItem1) && postData.body.includes(fakeMarkdownItem2)) .reply(201, commentResponse.payload, commentResponse.headers); if (githubReporter.complete) await githubReporter.complete(f.fakeOptions, f.fakeStats); expect(nock.isDone()).toBeTruthy(); }); test("one comment too large for github", async () => { f.fakeOptions["max-pr-suggestions"] = 0; await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/issues/1/comments", (postData) => postData.body.includes(MARKDOWN_RESPONSE)) .reply(422, commentResponseTooBig.payload, commentResponseTooBig.headers); let message = ""; if (githubReporter.complete) { try { await githubReporter.complete(f.fakeOptions, f.fakeStats); } catch (err) { message = err.message; } } expect(message).toEqual("The comment is too long for GitHub."); expect(nock.isDone()).toBeTruthy(); }); test("other github error posting comment", async () => { f.fakeOptions["max-pr-suggestions"] = 0; await githubReporter.issue(f.fakeItem, f.fakeOptions, f.fakeStats); nock("https://api.github.com:443", { encodedQueryParams: true }) .post("/repos/dprothero/testing-ground/issues/1/comments", (postData) => postData.body.includes(MARKDOWN_RESPONSE)) .reply(422, { ...commentResponseTooBig.payload, errors: [ { message: "Something else bad happened", }, ], }, commentResponseTooBig.headers); let message = ""; if (githubReporter.complete) { try { await githubReporter.complete(f.fakeOptions, f.fakeStats); } catch (err) { message = err.message; } } expect(message.startsWith("Validation Failed")).toBeTruthy(); expect(nock.isDone()).toBeTruthy(); }); test.run(); //# sourceMappingURL=githubReporter.test.js.map