From a80429a1cfd29f2c425e594a3eca90118b6d011a Mon Sep 17 00:00:00 2001 From: Andrew Hughes Date: Tue, 2 Jun 2020 17:49:13 +0100 Subject: [PATCH] lib/test: add initial prototype for wiby --test --- bin/wiby.js | 20 +++++- lib/github.js | 124 +++++++++++++++++++++++++++++++++++++ lib/test.js | 108 +++++++++++++++++++++++++++++++- package.json | 8 ++- test/fixtures/config.js | 48 ++++++++++++++ test/fixtures/package.json | 19 ++++++ test/github.js | 13 ++++ test/test.js | 50 ++++++++++++++- 8 files changed, 383 insertions(+), 7 deletions(-) create mode 100644 lib/github.js create mode 100644 test/fixtures/config.js create mode 100644 test/fixtures/package.json create mode 100644 test/github.js diff --git a/bin/wiby.js b/bin/wiby.js index 1f6c573..f1db21f 100755 --- a/bin/wiby.js +++ b/bin/wiby.js @@ -1,12 +1,26 @@ #!/usr/bin/env node +require('dotenv').config() const test = require('../lib/test') const result = require('../lib/result') -if (process.argv[2] === 'test') { - test() +const args = require('yargs') + .option('test', { + alias: 't', + describe: 'Test your dependents' + }) + .option('result', { + alias: 'r', + describe: 'Get the result of your tests' + }) + .argv + +if (args.test) { + test(args.test) } -if (process.argv[2] === 'result') { +if (args.result) { result() } + +// Usage: wiby --test URL diff --git a/lib/github.js b/lib/github.js new file mode 100644 index 0000000..1a01200 --- /dev/null +++ b/lib/github.js @@ -0,0 +1,124 @@ +const { graphql } = require('@octokit/graphql') +const { Octokit } = require('@octokit/rest') +const octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN +}) + +const graphqlWithAuth = graphql.defaults({ + headers: { + authorization: `token ${process.env.GITHUB_TOKEN}` + } +}) + +module.exports.getPackageJson = +async function getPackageJson (owner, repo) { + try { + const resp = await graphqlWithAuth({ + query: `query($owner: String!, $repo: String!) { + repository(name: $repo, owner: $owner) { + object(expression: "master:package.json") { + ... on Blob { + text + } + } + } + }`, + owner: owner, + repo: repo + }) + return (JSON.parse(resp.repository.object.text)) + } catch (err) { + if (err.errors && err.errors[0].type === 'NOT_FOUND') { + throw Error(`Could not find GitHub repository at https://www.github.com/${owner}/${repo}`) + } else { + throw err + } + } +} + +module.exports.getPermissions = +async function getPermissions (owner, repo) { + try { + const resp = await graphqlWithAuth({ + query: `query($owner: String!, $repo: String!) { + repository(name: $repo, owner: $owner) { + viewerPermission + } + }`, + owner: owner, + repo: repo + }) + return resp.repository.viewerPermission + } catch (err) { + if (err.errors && err.errors[0].type === 'NOT_FOUND') { + throw Error(`Could not find GitHub repository at https://www.github.com/${owner}/${repo}`) + } else { + throw err + } + } +} + +module.exports.getShas = +async function getShas (owner, repo) { + const resp = await octokit.repos.listCommits({ + owner, + repo, + per_page: 1 + }) + const headSha = resp.data[0].sha + const treeSha = resp.data[0].commit.tree.sha + // console.log(`lastSha: ${headSha} treeSha: ${treeSha}`) + return [headSha, treeSha] +} + +module.exports.createBlob = +async function createBlob (owner, repo, file) { + const { + data: { sha: blobSha } + } = await octokit.git.createBlob({ + owner, + repo, + content: file, + encoding: 'base64' + }) + return blobSha +} + +module.exports.createTree = async function createTree (owner, repo, treeSha, blobSha) { + const resp = await octokit.git.createTree({ + owner, + repo, + base_tree: treeSha, + tree: [ + { path: 'package.json', mode: '100644', sha: blobSha } + ], + headers: { + Accept: 'application/json' + } + }) + const newTreeSha = resp.data.sha + return newTreeSha +} + +module.exports.createCommit = +async function createCommit (owner, repo, message, newTreeSha, headSha) { + const resp = await octokit.git.createCommit({ + owner, + repo, + message: message, + tree: newTreeSha, + parents: [headSha] + }) + const commitSha = resp.data.sha + return commitSha +} + +module.exports.createBranch = +async function createBranch (owner, repo, commitSha, branch) { + await octokit.git.createRef({ + owner, + repo, + sha: commitSha, + ref: `refs/heads/${branch}` + }) +} diff --git a/lib/test.js b/lib/test.js index bd1c543..aa2f959 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,3 +1,107 @@ -module.exports = () => { - console.log('wiby test') +const fs = require('fs') +const fetch = require('node-fetch') +const { promisify } = require('util') +const github = require('./github') +const readFileAsync = promisify(fs.readFile) + +module.exports = async function (url) { + const dep = await getLocalPackageName() + console.log('Got local package name: ', dep) + const [org, repo] = getOrgRepo(url) + console.log(`Got org: ${org} and repo: ${repo}`) + const packageJSON = await github.getPackageJson(org, repo) + if (!checkPackageInPackageJSON(dep, packageJSON)) { + throw new Error('Dependency not found in package.json') + } + const patch = await getPatch(dep) + console.log('Created patch: ', patch) + const patchedPackageJSON = applyPatch(patch, dep, packageJSON) + await pushPatch(patchedPackageJSON, org, repo, dep) +} + +const getCommitHash = module.exports.getCommitHash = +async function getCommitHash (owner, repo) { + const headSha = (await github.getShas(owner, repo))[0] + return headSha +} + +const checkPackageInPackageJSON = module.exports.checkPackageInPackageJSON = +function checkPackageInPackageJSON (dep, packageJSON) { + return Object.prototype.hasOwnProperty.call(packageJSON.dependencies, dep) +} + +const getOrgRepo = module.exports.getOrgRepo = +function getOrgRepo (url) { + const repoOrgArr = (url.split('github.com'))[1].split('/') + const org = repoOrgArr[1] + const repo = repoOrgArr[2] + return [org, repo] +} + +const getPatch = module.exports.getPatch = +async function getPatch (dep, hash) { + dep = dep || await getLocalPackageName() + const [org, repo] = await getGitHubOrgRepo(dep) + hash = hash || await getCommitHash(org, repo) + const patch = `${org}/${repo}#${hash}` + return patch +} + +const getLocalPackageName = module.exports.getLocalPackageName = +async function getLocalPackageName (pkgPath) { + pkgPath = pkgPath || 'package.json' + let pkg = await readFileAsync(pkgPath).catch((err) => { + throw (err) + }) + pkg = JSON.parse(pkg) + return pkg.name +} + +const getGitHubOrgRepo = module.exports.getGitHubOrgRepo = +async function getGitHubOrgRepo (dep) { + const urlRegex = /github.com\/([^/])+\/[^/]+/g + const resp = await fetchRegistryInfo(dep) + let org, repo + if (resp.repository && resp.repository.url) { + let gitUrl = (resp.repository.url).match(urlRegex) + if (gitUrl) { + gitUrl = gitUrl[0].replace(/(\.git)/g, '') + org = getOrgRepo(gitUrl)[0] + repo = getOrgRepo(gitUrl)[1] + } + } + if (!org && !repo) { + org = 'undefined' + repo = 'undefined' + } + return [org, repo] +} + +const fetchRegistryInfo = module.exports.fetchRegistryInfo = +async function fetchRegistryInfo (dep) { + const resp = await fetch(`https://registry.npmjs.org/${dep}`) + return resp.json() +} + +const applyPatch = module.exports.applyPatch = +function applyPatch (patch, dep, packageJSON) { + if (!Object.prototype.hasOwnProperty.call(packageJSON.dependencies, dep)) { + throw new Error('Dependency not found in package.json') + } + packageJSON.dependencies[dep] = patch + return packageJSON +} + +async function pushPatch (packageJSON, owner, repo, dep) { + const file = JSON.stringify(packageJSON, null, 2) + '\n' // assumes package.json is using two spaces + const encodedFile = Buffer.from(file).toString('base64') + const message = `wiby: update ${dep}` + const branch = `wiby-${dep}` + + const [headSha, treeSha] = await github.getShas(owner, repo) + const blobSha = await github.createBlob(owner, repo, encodedFile) + const newTreeSha = await github.createTree(owner, repo, treeSha, blobSha) + const commitSha = await github.createCommit(owner, repo, message, newTreeSha, headSha) + await github.createBranch(owner, repo, commitSha, branch) + console.log(`Changes pushed to https://github.com/${owner}/${repo}/blob/${branch}/package.json`) } diff --git a/package.json b/package.json index 28fc6ca..4c0aa50 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,13 @@ "url": "https://github.com/pkgjs/wiby/issues" }, "homepage": "https://github.com/pkgjs/wiby#readme", - "dependencies": {}, + "dependencies": { + "@octokit/graphql": "^4.5.0", + "@octokit/rest": "^17.9.2", + "dotenv": "^8.2.0", + "node-fetch": "^2.6.0", + "yargs": "^15.3.1" + }, "devDependencies": { "standard": "^14.3.4", "tap": "^14.10.7" diff --git a/test/fixtures/config.js b/test/fixtures/config.js new file mode 100644 index 0000000..394fa8c --- /dev/null +++ b/test/fixtures/config.js @@ -0,0 +1,48 @@ +module.exports.DEP_URL = 'https://github.com/andrewhughes101/do-you-pass' +module.exports.PKGJSON = { + name: 'do-you-pass', + version: '1.0.0', + description: 'Does i-should-pass pass?', + main: 'index.js', + scripts: { + test: 'node test.js' + }, + author: 'Andrew Hughes', + license: 'Apache-2.0', + homepage: 'https://github.com/andrewhughes101/do-you-pass', + repository: { + type: 'git', + url: 'https://github.com/andrewhughes101/do-you-pass.git' + }, + dependencies: { + 'i-should-pass': '^1.0.0' + } +} +module.exports.PATCHED_PKGJSON = { + name: 'do-you-pass', + version: '1.0.0', + description: 'Does i-should-pass pass?', + main: 'index.js', + scripts: { + test: 'node test.js' + }, + author: 'Andrew Hughes', + license: 'Apache-2.0', + homepage: 'https://github.com/andrewhughes101/do-you-pass', + repository: { + type: 'git', + url: 'https://github.com/andrewhughes101/do-you-pass.git' + }, + dependencies: { + 'i-should-pass': 'andrewhughes101/i-should-pass#ec218ed4d7bd085c4aa3d94c2f86a43470754816' + } +} +module.exports.PATCH = 'andrewhughes101/i-should-pass#ec218ed4d7bd085c4aa3d94c2f86a43470754816' +module.exports.PKG_NAME = 'i-should-pass' +module.exports.PKG_REPO = 'i-should-pass' +module.exports.PKG_ORG = 'andrewhughes101' +module.exports.DEP_REPO = 'do-you-pass' +module.exports.DEP_ORG = 'andrewhughes101' +module.exports.PKG_HEAD_SHA = 'ec218ed4d7bd085c4aa3d94c2f86a43470754816' +module.exports.LOCAL = 'i-should-pass' +module.exports.DEP_REPO_PERM = 'ADMIN' diff --git a/test/fixtures/package.json b/test/fixtures/package.json new file mode 100644 index 0000000..e75be14 --- /dev/null +++ b/test/fixtures/package.json @@ -0,0 +1,19 @@ +{ + "name": "do-you-pass", + "version": "1.0.0", + "description": "Does i-should-pass pass?", + "main": "index.js", + "scripts": { + "test": "node test.js" + }, + "author": "Andrew Hughes", + "license": "Apache-2.0", + "homepage": "https://github.com/andrewhughes101/do-you-pass", + "repository": { + "type": "git", + "url": "https://github.com/andrewhughes101/do-you-pass.git" + }, + "dependencies": { + "i-should-pass": "^1.0.0" + } +} diff --git a/test/github.js b/test/github.js new file mode 100644 index 0000000..e960891 --- /dev/null +++ b/test/github.js @@ -0,0 +1,13 @@ +require('dotenv').config() +const tap = require('tap') +const github = require('../lib/github') +const CONFIG = require('./fixtures/config') + +tap.test('package.json can be fetched with a valid url', async tap => { + tap.equal(JSON.stringify(await github.getPackageJson(CONFIG.DEP_ORG, CONFIG.DEP_REPO)), JSON.stringify(CONFIG.PKGJSON)) + // tap.throws( await pkgTest.getPackageJson('not-an-org', 'not-a-repo')) +}) + +tap.test('correct permissions returned for GitHub repo', async tap => { + tap.equal((await github.getPermissions(CONFIG.DEP_ORG, CONFIG.DEP_REPO)), CONFIG.DEP_REPO_PERM) +}) diff --git a/test/test.js b/test/test.js index 06a3d97..c0f3c44 100644 --- a/test/test.js +++ b/test/test.js @@ -1,2 +1,50 @@ +require('dotenv').config() const tap = require('tap') -tap.pass('this is fine') +const path = require('path') +const CONFIG = require('./fixtures/config') +const localPkg = require('./fixtures/package.json') +const pkgTest = require('../lib/test') + +tap.test('Test correct sha returned for a GitHub repository', async tap => { + tap.equal(await pkgTest.getCommitHash(CONFIG.PKG_ORG, CONFIG.PKG_REPO), CONFIG.PKG_HEAD_SHA) +}) + +tap.test('Check if the dependency is listed in the package.json', tap => { + tap.equal(pkgTest.checkPackageInPackageJSON(CONFIG.PKG_NAME, CONFIG.PKGJSON), true) + tap.equal(pkgTest.checkPackageInPackageJSON('not-a-dep', CONFIG.PKGJSON), false) + tap.end() +}) + +tap.test('GitHub organisation/owner and repository returned', tap => { + tap.equal(pkgTest.getOrgRepo(CONFIG.DEP_URL)[0], CONFIG.DEP_ORG) + tap.equal(pkgTest.getOrgRepo(CONFIG.DEP_URL)[1], CONFIG.DEP_REPO) + tap.end() +}) + +tap.test('patch created from github org repo and commit sha', async tap => { + tap.equal(await pkgTest.getPatch(CONFIG.PKG_NAME, CONFIG.PKG_HEAD_SHA), CONFIG.PATCH) +}) + +tap.test('Local package.json name returned correctly', async tap => { + const pkgPath = path.join(__dirname, '/fixtures/package.json') + tap.equal(await pkgTest.getLocalPackageName(pkgPath), localPkg.name) +}) + +tap.test('GitHub org and repo returned when given package name', async tap => { + tap.equal((await pkgTest.getGitHubOrgRepo(CONFIG.PKG_NAME))[0], CONFIG.PKG_ORG) + tap.equal((await pkgTest.getGitHubOrgRepo(CONFIG.PKG_NAME))[1], CONFIG.PKG_REPO) +}) + +tap.test('Check registry info is fetched correctly', async tap => { + tap.equal((await pkgTest.fetchRegistryInfo(CONFIG.PKG_NAME))._id, CONFIG.PKG_NAME) +}) + +tap.test('undefined returned when given an invalid package name', async tap => { + tap.equal((await pkgTest.getGitHubOrgRepo('not-a-package'))[0], 'undefined') + tap.equal((await pkgTest.getGitHubOrgRepo('not-a-package'))[1], 'undefined') +}) + +tap.test('Check patch applied to package.json successfully', tap => { + tap.equal(JSON.stringify(pkgTest.applyPatch(CONFIG.PATCH, CONFIG.PKG_NAME, CONFIG.PKGJSON)), JSON.stringify(CONFIG.PATCHED_PKGJSON)) + tap.end() +})