Add some simple unit tests

This commit is contained in:
Jonathan Clem
2022-02-01 22:08:12 +00:00
committed by GitHub
parent 03fb990f28
commit b39b67e25e
8 changed files with 342 additions and 138 deletions

View File

@@ -0,0 +1,148 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import {addToProject} from '../src/add-to-project'
describe('addToProject', () => {
let outputs: Record<string, string>
beforeEach(() => {
jest.spyOn(process.stdout, 'write').mockImplementation(() => true)
})
beforeEach(() => {
mockGetInput({
'project-url': 'https://github.com/orgs/github/projects/1',
'github-token': 'gh_token'
})
outputs = mockSetOutput()
})
afterEach(() => {
github.context.payload = {}
jest.restoreAllMocks()
})
test('adds an issue to the project', async () => {
mockGraphQL(
{
test: /getProject/,
return: {
organization: {
projectNext: {
id: 'project-next-id'
}
}
}
},
{
test: /addProjectNextItem/,
return: {
addProjectNextItem: {
projectNextItem: {
id: 'project-next-item-id'
}
}
}
}
)
await addToProject()
expect(outputs.itemId).toEqual('project-next-item-id')
})
test('adds matching issues with a label filter', async () => {
mockGetInput({
'project-url': 'https://github.com/orgs/github/projects/1',
'github-token': 'gh_token',
labeled: 'bug'
})
github.context.payload = {
issue: {
number: 1,
labels: [{name: 'bug'}]
}
}
mockGraphQL(
{
test: /getProject/,
return: {
organization: {
projectNext: {
id: 'project-next-id'
}
}
}
},
{
test: /addProjectNextItem/,
return: {
addProjectNextItem: {
projectNextItem: {
id: 'project-next-item-id'
}
}
}
}
)
await addToProject()
expect(outputs.itemId).toEqual('project-next-item-id')
})
test('does not add un-matching issues with a label filter', async () => {
mockGetInput({
'project-url': 'https://github.com/orgs/github/projects/1',
'github-token': 'gh_token',
labeled: 'bug'
})
github.context.payload = {
issue: {
number: 1,
labels: []
}
}
const infoSpy = jest.spyOn(core, 'info')
const gqlMock = mockGraphQL()
await addToProject()
expect(infoSpy).toHaveBeenCalledWith(`Skipping issue 1 because it does not have one of the labels: bug`)
expect(gqlMock).not.toHaveBeenCalled()
})
})
function mockGetInput(mocks: Record<string, string>): jest.SpyInstance {
const mock = (key: string) => mocks[key] ?? ''
return jest.spyOn(core, 'getInput').mockImplementation(mock)
}
function mockSetOutput(): Record<string, string> {
const output: Record<string, string> = {}
jest.spyOn(core, 'setOutput').mockImplementation((key, value) => (output[key] = value))
return output
}
function mockGraphQL(...mocks: {test: RegExp; return: unknown}[]): jest.Mock {
const mock = jest.fn().mockImplementation((query: string) => {
const match = mocks.find(m => m.test.test(query))
if (match) {
return match.return
}
throw new Error(`Unexpected GraphQL query: ${query}`)
})
jest.spyOn(github, 'getOctokit').mockImplementation(() => {
return {
graphql: mock
} as unknown as ReturnType<typeof github.getOctokit>
})
return mock
}

View File

@@ -1,5 +0,0 @@
import {expect, test} from '@jest/globals'
test('sanity check', () => {
expect(true).toBeTruthy()
})

53
dist/index.js generated vendored
View File

@@ -1,7 +1,7 @@
require('./sourcemap-register.js');/******/ (() => { // webpackBootstrap require('./sourcemap-register.js');/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({ /******/ var __webpack_modules__ = ({
/***/ 3109: /***/ 6672:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict"; "use strict";
@@ -35,12 +35,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
}); });
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.addToProject = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const github = __importStar(__nccwpck_require__(5438)); const github = __importStar(__nccwpck_require__(5438));
// TODO: Ensure this (and the Octokit client) works for non-github.com URLs, as well. // TODO: Ensure this (and the Octokit client) works for non-github.com URLs, as well.
// https://github.com/orgs|users/<ownerName>/projects/<projectNumber> // https://github.com/orgs|users/<ownerName>/projects/<projectNumber>
const urlParse = /^(?:https:\/\/)?github\.com\/(?<ownerType>orgs|users)\/(?<ownerName>[^/]+)\/projects\/(?<projectNumber>\d+)/; const urlParse = /^(?:https:\/\/)?github\.com\/(?<ownerType>orgs|users)\/(?<ownerName>[^/]+)\/projects\/(?<projectNumber>\d+)/;
function run() { function addToProject() {
var _a, _b, _c, _d, _e, _f, _g, _h; var _a, _b, _c, _d, _e, _f, _g, _h;
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const projectUrl = core.getInput('project-url', { required: true }); const projectUrl = core.getInput('project-url', { required: true });
@@ -104,14 +105,7 @@ function run() {
core.setOutput('itemId', addResp.addProjectNextItem.projectNextItem.id); core.setOutput('itemId', addResp.addProjectNextItem.projectNextItem.id);
}); });
} }
run() exports.addToProject = addToProject;
.catch(err => {
core.setFailed(err.message);
process.exit(1);
})
.then(() => {
process.exit(0);
});
function mustGetOwnerTypeQuery(ownerType) { function mustGetOwnerTypeQuery(ownerType) {
const ownerTypeQuery = ownerType === 'orgs' ? 'organization' : ownerType === 'users' ? 'user' : null; const ownerTypeQuery = ownerType === 'orgs' ? 'organization' : ownerType === 'users' ? 'user' : null;
if (!ownerTypeQuery) { if (!ownerTypeQuery) {
@@ -121,6 +115,45 @@ function mustGetOwnerTypeQuery(ownerType) {
} }
/***/ }),
/***/ 3109:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(2186));
const add_to_project_1 = __nccwpck_require__(6672);
(0, add_to_project_1.addToProject)()
.catch(err => {
core.setFailed(err.message);
process.exit(1);
})
.then(() => {
process.exit(0);
});
/***/ }), /***/ }),
/***/ 7351: /***/ 7351:

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

25
package-lock.json generated
View File

@@ -14,6 +14,7 @@
}, },
"devDependencies": { "devDependencies": {
"@github/prettier-config": "^0.0.4", "@github/prettier-config": "^0.0.4",
"@types/jest": "^27.4.0",
"@types/node": "^12.12.6", "@types/node": "^12.12.6",
"@typescript-eslint/parser": "^5.10.2", "@typescript-eslint/parser": "^5.10.2",
"@vercel/ncc": "^0.33.1", "@vercel/ncc": "^0.33.1",
@@ -26,6 +27,10 @@
"prettier": "2.5.1", "prettier": "2.5.1",
"ts-jest": "^27.1.3", "ts-jest": "^27.1.3",
"typescript": "^4.5.5" "typescript": "^4.5.5"
},
"engines": {
"node": ">=10.0.0 <17.0.0",
"npm": ">= 7.0.0"
} }
}, },
"node_modules/@actions/core": { "node_modules/@actions/core": {
@@ -1305,6 +1310,16 @@
"@types/istanbul-lib-report": "*" "@types/istanbul-lib-report": "*"
} }
}, },
"node_modules/@types/jest": {
"version": "27.4.0",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz",
"integrity": "sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==",
"dev": true,
"dependencies": {
"jest-diff": "^27.0.0",
"pretty-format": "^27.0.0"
}
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.9", "version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@@ -7324,6 +7339,16 @@
"@types/istanbul-lib-report": "*" "@types/istanbul-lib-report": "*"
} }
}, },
"@types/jest": {
"version": "27.4.0",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz",
"integrity": "sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==",
"dev": true,
"requires": {
"jest-diff": "^27.0.0",
"pretty-format": "^27.0.0"
}
},
"@types/json-schema": { "@types/json-schema": {
"version": "7.0.9", "version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",

View File

@@ -13,6 +13,7 @@
}, },
"devDependencies": { "devDependencies": {
"@github/prettier-config": "^0.0.4", "@github/prettier-config": "^0.0.4",
"@types/jest": "^27.4.0",
"@types/node": "^12.12.6", "@types/node": "^12.12.6",
"@typescript-eslint/parser": "^5.10.2", "@typescript-eslint/parser": "^5.10.2",
"@vercel/ncc": "^0.33.1", "@vercel/ncc": "^0.33.1",

122
src/add-to-project.ts Normal file
View File

@@ -0,0 +1,122 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
// TODO: Ensure this (and the Octokit client) works for non-github.com URLs, as well.
// https://github.com/orgs|users/<ownerName>/projects/<projectNumber>
const urlParse =
/^(?:https:\/\/)?github\.com\/(?<ownerType>orgs|users)\/(?<ownerName>[^/]+)\/projects\/(?<projectNumber>\d+)/
interface ProjectNodeIDResponse {
organization?: {
projectNext: {
id: string
}
}
user?: {
projectNext: {
id: string
}
}
}
interface ProjectAddItemResponse {
addProjectNextItem: {
projectNextItem: {
id: string
}
}
}
export async function addToProject(): Promise<void> {
const projectUrl = core.getInput('project-url', {required: true})
const ghToken = core.getInput('github-token', {required: true})
const labeled =
core
.getInput('labeled')
.split(',')
.map(l => l.trim())
.filter(l => l.length > 0) ?? []
const octokit = github.getOctokit(ghToken)
const urlMatch = projectUrl.match(urlParse)
const issue = github.context.payload.issue ?? github.context.payload.pull_request
const issueLabels: string[] = (issue?.labels ?? []).map((l: {name: string}) => l.name)
// Ensure the issue matches our `labeled` filter, if provided.
if (labeled.length > 0) {
const hasLabel = issueLabels.some(l => labeled.includes(l))
if (!hasLabel) {
core.info(`Skipping issue ${issue?.number} because it does not have one of the labels: ${labeled.join(', ')}`)
return
}
}
core.debug(`Project URL: ${projectUrl}`)
if (!urlMatch) {
throw new Error(
`Invalid project URL: ${projectUrl}. Project URL should match the format https://github.com/<orgs-or-users>/<ownerName>/projects/<projectNumber>`
)
}
const ownerName = urlMatch.groups?.ownerName
const projectNumber = parseInt(urlMatch.groups?.projectNumber ?? '', 10)
const ownerType = urlMatch.groups?.ownerType
const ownerTypeQuery = mustGetOwnerTypeQuery(ownerType)
core.debug(`Org name: ${ownerName}`)
core.debug(`Project number: ${projectNumber}`)
core.debug(`Owner type: ${ownerType}`)
// First, use the GraphQL API to request the project's node ID.
const idResp = await octokit.graphql<ProjectNodeIDResponse>(
`query getProject($ownerName: String!, $projectNumber: Int!) {
${ownerTypeQuery}(login: $ownerName) {
projectNext(number: $projectNumber) {
id
}
}
}`,
{
ownerName,
projectNumber
}
)
const projectId = idResp[ownerTypeQuery]?.projectNext.id
const contentId = issue?.node_id
core.debug(`Project node ID: ${projectId}`)
core.debug(`Content ID: ${contentId}`)
// Next, use the GraphQL API to add the issue to the project.
const addResp = await octokit.graphql<ProjectAddItemResponse>(
`mutation addIssueToProject($input: AddProjectNextItemInput!) {
addProjectNextItem(input: $input) {
projectNextItem {
id
}
}
}`,
{
input: {
contentId,
projectId
}
}
)
core.setOutput('itemId', addResp.addProjectNextItem.projectNextItem.id)
}
function mustGetOwnerTypeQuery(ownerType?: string): 'organization' | 'user' {
const ownerTypeQuery = ownerType === 'orgs' ? 'organization' : ownerType === 'users' ? 'user' : null
if (!ownerTypeQuery) {
throw new Error(`Unsupported ownerType: ${ownerType}. Must be one of 'orgs' or 'users'`)
}
return ownerTypeQuery
}

View File

@@ -1,117 +1,7 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as github from '@actions/github' import {addToProject} from './add-to-project'
// TODO: Ensure this (and the Octokit client) works for non-github.com URLs, as well. addToProject()
// https://github.com/orgs|users/<ownerName>/projects/<projectNumber>
const urlParse =
/^(?:https:\/\/)?github\.com\/(?<ownerType>orgs|users)\/(?<ownerName>[^/]+)\/projects\/(?<projectNumber>\d+)/
interface ProjectNodeIDResponse {
organization?: {
projectNext: {
id: string
}
}
user?: {
projectNext: {
id: string
}
}
}
interface ProjectAddItemResponse {
addProjectNextItem: {
projectNextItem: {
id: string
}
}
}
async function run(): Promise<void> {
const projectUrl = core.getInput('project-url', {required: true})
const ghToken = core.getInput('github-token', {required: true})
const labeled =
core
.getInput('labeled')
.split(',')
.map(l => l.trim())
.filter(l => l.length > 0) ?? []
const octokit = github.getOctokit(ghToken)
const urlMatch = projectUrl.match(urlParse)
const issue = github.context.payload.issue ?? github.context.payload.pull_request
const issueLabels: string[] = (issue?.labels ?? []).map((l: {name: string}) => l.name)
// Ensure the issue matches our `labeled` filter, if provided.
if (labeled.length > 0) {
const hasLabel = issueLabels.some(l => labeled.includes(l))
if (!hasLabel) {
core.info(`Skipping issue ${issue?.number} because it does not have one of the labels: ${labeled.join(', ')}`)
return
}
}
core.debug(`Project URL: ${projectUrl}`)
if (!urlMatch) {
throw new Error(
`Invalid project URL: ${projectUrl}. Project URL should match the format https://github.com/<orgs-or-users>/<ownerName>/projects/<projectNumber>`
)
}
const ownerName = urlMatch.groups?.ownerName
const projectNumber = parseInt(urlMatch.groups?.projectNumber ?? '', 10)
const ownerType = urlMatch.groups?.ownerType
const ownerTypeQuery = mustGetOwnerTypeQuery(ownerType)
core.debug(`Org name: ${ownerName}`)
core.debug(`Project number: ${projectNumber}`)
core.debug(`Owner type: ${ownerType}`)
// First, use the GraphQL API to request the project's node ID.
const idResp = await octokit.graphql<ProjectNodeIDResponse>(
`query getProject($ownerName: String!, $projectNumber: Int!) {
${ownerTypeQuery}(login: $ownerName) {
projectNext(number: $projectNumber) {
id
}
}
}`,
{
ownerName,
projectNumber
}
)
const projectId = idResp[ownerTypeQuery]?.projectNext.id
const contentId = issue?.node_id
core.debug(`Project node ID: ${projectId}`)
core.debug(`Content ID: ${contentId}`)
// Next, use the GraphQL API to add the issue to the project.
const addResp = await octokit.graphql<ProjectAddItemResponse>(
`mutation addIssueToProject($input: AddProjectNextItemInput!) {
addProjectNextItem(input: $input) {
projectNextItem {
id
}
}
}`,
{
input: {
contentId,
projectId
}
}
)
core.setOutput('itemId', addResp.addProjectNextItem.projectNextItem.id)
}
run()
.catch(err => { .catch(err => {
core.setFailed(err.message) core.setFailed(err.message)
process.exit(1) process.exit(1)
@@ -119,13 +9,3 @@ run()
.then(() => { .then(() => {
process.exit(0) process.exit(0)
}) })
function mustGetOwnerTypeQuery(ownerType?: string): 'organization' | 'user' {
const ownerTypeQuery = ownerType === 'orgs' ? 'organization' : ownerType === 'users' ? 'user' : null
if (!ownerTypeQuery) {
throw new Error(`Unsupported ownerType: ${ownerType}. Must be one of 'orgs' or 'users'`)
}
return ownerTypeQuery
}