From 11ce557d95faf1583eaba6f42456fae0ffed7cc9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mislav=20Marohni=C4=87?= <git@mislav.net>
Date: Sat, 27 Feb 2021 18:06:00 +0100
Subject: [PATCH] Refactor to be more testable

---
 package.json                       | 15 +++---
 src/api.ts                         | 33 ++++++------
 src/calculate-download-checksum.ts |  5 +-
 src/edit-github-blob.ts            | 13 +++--
 src/main-test.ts                   | 81 ++++++++++++++++++++++++++++++
 src/main.ts                        | 53 +++++++++++++------
 src/replace-formula-fields-test.ts | 22 ++++++++
 src/test.ts                        | 61 ----------------------
 src/version-test.ts                | 17 +++++++
 9 files changed, 196 insertions(+), 104 deletions(-)
 create mode 100644 src/main-test.ts
 create mode 100644 src/replace-formula-fields-test.ts
 delete mode 100644 src/test.ts
 create mode 100644 src/version-test.ts

diff --git a/package.json b/package.json
index 925c649..bf5edaf 100644
--- a/package.json
+++ b/package.json
@@ -3,16 +3,18 @@
   "scripts": {
     "build": "rm -rf lib && ncc build src/run.ts -o lib --source-map",
     "lint": "eslint --ext '.js,.ts' .",
-    "test": "tsc && ava"
+    "test": "tsc --sourceMap && ava"
   },
   "dependencies": {
-    "@actions/core": "^1.2.4",
-    "@octokit/core": "^3.1.1",
-    "@octokit/plugin-request-log": "^1.0.0",
-    "@octokit/plugin-rest-endpoint-methods": "^4.1.2"
+    "@actions/core": "^1.2.6",
+    "@actions/github": "^4.0.0",
+    "@octokit/core": "^3.2.5",
+    "@octokit/plugin-request-log": "^1.0.3",
+    "@octokit/plugin-rest-endpoint-methods": "^4.10.1"
   },
   "devDependencies": {
-    "@types/node": "^12.7.5",
+    "@types/node": "^14.14.25",
+    "@types/node-fetch": "^2.5.8",
     "@typescript-eslint/eslint-plugin": "^3.7.1",
     "@typescript-eslint/parser": "^3.7.1",
     "@zeit/ncc": "^0.22.3",
@@ -20,6 +22,7 @@
     "eslint": "^7.5.0",
     "eslint-config-prettier": "^6.11.0",
     "eslint-plugin-prettier": "^3.1.4",
+    "node-fetch": "^2.6.1",
     "prettier": "^2.0.5",
     "typescript": "^3.9.7"
   },
diff --git a/src/api.ts b/src/api.ts
index 1956966..51e9d0a 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -9,23 +9,24 @@ const GitHub = Octokit.plugin(restEndpointMethods, requestLog).defaults({
 
 export type API = InstanceType<typeof GitHub>
 
-type LogMethod = (msg: any, ...params: any[]) => void
-
-type Logger = {
-  info?: LogMethod
-  debug?: LogMethod
-}
-
-export default function (token: string): API {
-  const log: Logger = {
-    info: console.log,
-  }
-  if (isDebug()) {
-    log.debug = console.debug
-  }
-
+export default function (token: string, options?: {fetch?: any}): API {
   return new GitHub({
+    request: {fetch: options?.fetch},
     auth: `token ${token}`,
-    log,
+    log: {
+      info(msg: string) {
+        return console.info(msg)
+      },
+      debug(msg: string) {
+        if (!isDebug()) return
+        return console.debug(msg)
+      },
+      warn(msg: string) {
+        return console.warn(msg)
+      },
+      error(msg: string) {
+        return console.error(msg)
+      },
+    },
   })
 }
diff --git a/src/calculate-download-checksum.ts b/src/calculate-download-checksum.ts
index 3fcad8b..25a1402 100644
--- a/src/calculate-download-checksum.ts
+++ b/src/calculate-download-checksum.ts
@@ -39,10 +39,11 @@ async function resolveDownload(api: API, url: URL): Promise<URL> {
     )
     if (archive != null) {
       const [, owner, repo, ref, ext] = archive
-      const res = await api.repos.downloadArchive({
+      const res = await (ext == '.zip'
+        ? api.repos.downloadZipballArchive
+        : api.repos.downloadTarballArchive)({
         owner,
         repo,
-        archive_format: ext == '.zip' ? 'zipball' : 'tarball',
         ref,
         request: {
           redirect: 'manual',
diff --git a/src/edit-github-blob.ts b/src/edit-github-blob.ts
index db153e9..572ab81 100644
--- a/src/edit-github-blob.ts
+++ b/src/edit-github-blob.ts
@@ -20,7 +20,7 @@ async function retry<T>(
   }
 }
 
-type Options = {
+export type Options = {
   owner: string
   repo: string
   filePath: string
@@ -47,12 +47,15 @@ export default async function (params: Options): Promise<string> {
     branch: baseBranch,
   })
 
-  const needsFork = !repoRes.data.permissions.push
+  const needsFork = !repoRes.data.permissions?.push
   if (needsFork) {
-    const forkRes = await api.repos.createFork(baseRepo)
+    const res = await Promise.all([
+      api.repos.createFork(baseRepo),
+      api.users.getAuthenticated(),
+    ])
     headRepo = {
-      owner: forkRes.data.owner.login,
-      repo: forkRes.data.name,
+      owner: res[1].data.login,
+      repo: baseRepo.repo,
     }
   }
 
diff --git a/src/main-test.ts b/src/main-test.ts
new file mode 100644
index 0000000..e58b30e
--- /dev/null
+++ b/src/main-test.ts
@@ -0,0 +1,81 @@
+import test from 'ava'
+import api from './api'
+import { commitForRelease, prepareEdit } from './main'
+import { Response } from 'node-fetch'
+
+test('commitForRelease()', (t) => {
+  t.is(
+    commitForRelease('This is a fixed commit message', {
+      formulaName: 'test formula',
+    }),
+    'This is a fixed commit message'
+  )
+  t.is(
+    commitForRelease('chore({{formulaName}}): version {{version}}', {
+      formulaName: 'test formula',
+    }),
+    'chore(test formula): version {{version}}'
+  )
+  t.is(
+    commitForRelease('chore({{formulaName}}): upgrade to version {{version}}', {
+      formulaName: 'test formula',
+      version: 'v1.2.3',
+    }),
+    'chore(test formula): upgrade to version v1.2.3'
+  )
+})
+
+test('prepareEdit()', async (t) => {
+  const ctx = {
+    sha: 'TAGSHA',
+    ref: 'refs/tags/v0.8.2',
+    repo: {
+      owner: 'OWNER',
+      repo: 'REPO',
+    },
+  }
+
+  process.env['INPUT_HOMEBREW-TAP'] = 'Homebrew/homebrew-core'
+  process.env['INPUT_COMMIT-MESSAGE'] = 'Upgrade {{formulaName}} to {{version}}'
+
+  // FIXME: this tests results in a live HTTP request. Figure out how to stub the `stream()` method in
+  // calculate-download-checksum.
+  const stubbedFetch = function (url: string) {
+    if (url == 'https://api.github.com/repos/OWNER/REPO/tarball/v0.8.2') {
+      return Promise.resolve(
+        new Response('', {
+          status: 301,
+          headers: {
+            Location:
+              'https://github.com/mislav/bump-homebrew-formula-action/archive/v1.9.tar.gz',
+          },
+        })
+      )
+    }
+    throw url
+  }
+  const apiClient = api('ATOKEN', { fetch: stubbedFetch })
+
+  const opts = await prepareEdit(ctx, apiClient, apiClient)
+  t.is(opts.owner, 'Homebrew')
+  t.is(opts.repo, 'homebrew-core')
+  t.is(opts.branch, '')
+  t.is(opts.filePath, 'Formula/repo.rb')
+  t.is(opts.commitMessage, 'Upgrade repo to 0.8.2')
+
+  const oldFormula = `
+    class MyProgram < Formula
+      url "OLDURL"
+      sha256 "OLDSHA"
+    end
+  `
+  t.is(
+    opts.replace(oldFormula),
+    `
+    class MyProgram < Formula
+      url "https://github.com/OWNER/REPO/archive/v0.8.2.tar.gz"
+      sha256 "c036fbc44901b266f6d408d6ca36ba56f63c14cc97994a935fb9741b55edee83"
+    end
+  `
+  )
+})
diff --git a/src/main.ts b/src/main.ts
index b0867dc..153befa 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,8 +1,10 @@
 import { getInput } from '@actions/core'
 import type { API } from './api'
 import editGitHubBlob from './edit-github-blob'
+import { Options as EditOptions } from './edit-github-blob'
 import { replaceFields } from './replace-formula-fields'
 import calculateDownloadChecksum from './calculate-download-checksum'
+import { context } from '@actions/github'
 
 function tarballForRelease(
   owner: string,
@@ -27,19 +29,34 @@ export default async function (api: (token: string) => API): Promise<void> {
     process.env.GITHUB_TOKEN || process.env.COMMITTER_TOKEN || ''
   const externalToken = process.env.COMMITTER_TOKEN || ''
 
-  const [contextOwner, contextRepoName] = (process.env
-    .GITHUB_REPOSITORY as string).split('/')
+  const options = await prepareEdit(context, api(internalToken), api(externalToken))
+  const createdUrl = await editGitHubBlob(options)
+  console.log(createdUrl)
+}
+
+type Context = {
+  ref: string
+  sha: string
+  repo: {
+    owner: string
+    repo: string
+  }
+}
+
+export async function prepareEdit(ctx: Context, sameRepoClient: API, crossRepoClient: API): Promise<EditOptions> {
+  const tagName = getInput('tag-name') || ((ref) => {
+    if (!ref.startsWith('refs/tags/')) throw `invalid ref: ${ref}`
+    return ref.replace('refs/tags/', '')
+  })(ctx.ref)
 
   const [owner, repo] = getInput('homebrew-tap', { required: true }).split('/')
-  const formulaName = getInput('formula-name') || contextRepoName.toLowerCase()
+  const formulaName = getInput('formula-name') || ctx.repo.repo.toLowerCase()
   const branch = getInput('base-branch')
   const filePath = `Formula/${formulaName}.rb`
-  const tagName = (process.env.GITHUB_REF as string).replace('refs/tags/', '')
-  const tagSha = process.env.GITHUB_SHA as string
-  const version = getInput('tag-name') || tagName.replace(/^v(\d)/, '$1')
+  const version = tagName.replace(/^v(\d)/, '$1')
   const downloadUrl =
     getInput('download-url') ||
-    tarballForRelease(contextOwner, contextRepoName, tagName)
+    tarballForRelease(ctx.repo.owner, ctx.repo.repo, tagName)
   const messageTemplate = getInput('commit-message', { required: true })
 
   const replacements = new Map<string, string>()
@@ -47,11 +64,20 @@ export default async function (api: (token: string) => API): Promise<void> {
   replacements.set('url', downloadUrl)
   if (downloadUrl.endsWith('.git')) {
     replacements.set('tag', tagName)
-    replacements.set('revision', tagSha)
+    replacements.set('revision', await (async () => {
+      if (ctx.ref == `refs/tags/${tagName}`) return ctx.sha
+      else {
+        const res = await sameRepoClient.git.getRef({
+          ...ctx.repo,
+          ref: `tags/${tagName}`
+        })
+        return res.data.object.sha
+      }
+    })())
   } else {
     replacements.set(
       'sha256',
-      await calculateDownloadChecksum(api(internalToken), downloadUrl, 'sha256')
+      await calculateDownloadChecksum(sameRepoClient, downloadUrl, 'sha256')
     )
   }
 
@@ -60,16 +86,15 @@ export default async function (api: (token: string) => API): Promise<void> {
     version,
   })
 
-  const createdUrl = await editGitHubBlob({
-    apiClient: api(externalToken),
+  return {
+    apiClient: crossRepoClient,
     owner,
     repo,
     branch,
     filePath,
     commitMessage,
-    replace(oldContent) {
+    replace(oldContent: string) {
       return replaceFields(oldContent, replacements)
     },
-  })
-  console.log(createdUrl)
+  }
 }
diff --git a/src/replace-formula-fields-test.ts b/src/replace-formula-fields-test.ts
new file mode 100644
index 0000000..ef615ba
--- /dev/null
+++ b/src/replace-formula-fields-test.ts
@@ -0,0 +1,22 @@
+import test from 'ava'
+import { replaceFields } from './replace-formula-fields'
+
+test('replaceFields()', (t) => {
+  const input = `
+  url "https://github.com/old/url.git",
+    tag: 'v0.9.0',
+    revision => "OLDREV"
+`
+  const expected = `
+  url "https://github.com/cli/cli.git",
+    tag: 'v0.11.1',
+    revision => "NEWREV"
+`
+
+  const replacements = new Map<string, string>()
+  replacements.set('url', 'https://github.com/cli/cli.git')
+  replacements.set('tag', 'v0.11.1')
+  replacements.set('revision', 'NEWREV')
+
+  t.is(replaceFields(input, replacements), expected)
+})
diff --git a/src/test.ts b/src/test.ts
deleted file mode 100644
index d702ae7..0000000
--- a/src/test.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import test from 'ava'
-import { commitForRelease } from './main'
-import { fromUrl } from './version'
-import { replaceFields } from './replace-formula-fields'
-
-test('version.fromUrl()', (t) => {
-  t.is(
-    fromUrl('https://github.com/me/myproject/archive/v1.2.3.tar.gz'),
-    'v1.2.3'
-  )
-  t.is(
-    fromUrl(
-      'https://github.com/me/myproject/releases/download/v1.2.3/file.tgz'
-    ),
-    'v1.2.3'
-  )
-  t.is(fromUrl('http://myproject.net/download/v1.2.3.tgz'), 'v1.2.3')
-  t.is(fromUrl('https://example.com/v1.2.3.zip'), 'v1.2.3')
-})
-
-test('main.commitForRelease()', (t) => {
-  t.is(
-    commitForRelease('This is a fixed commit message', {
-      formulaName: 'test formula',
-    }),
-    'This is a fixed commit message'
-  )
-  t.is(
-    commitForRelease('chore({{formulaName}}): version {{version}}', {
-      formulaName: 'test formula',
-    }),
-    'chore(test formula): version {{version}}'
-  )
-  t.is(
-    commitForRelease('chore({{formulaName}}): upgrade to version {{version}}', {
-      formulaName: 'test formula',
-      version: 'v1.2.3',
-    }),
-    'chore(test formula): upgrade to version v1.2.3'
-  )
-})
-
-test('replace-formula-fields.replaceFields()', (t) => {
-  const input = `
-  url "https://github.com/old/url.git",
-    tag: 'v0.9.0',
-    revision => "OLDREV"
-`
-  const expected = `
-  url "https://github.com/cli/cli.git",
-    tag: 'v0.11.1',
-    revision => "NEWREV"
-`
-
-  const replacements = new Map<string, string>()
-  replacements.set('url', 'https://github.com/cli/cli.git')
-  replacements.set('tag', 'v0.11.1')
-  replacements.set('revision', 'NEWREV')
-
-  t.is(replaceFields(input, replacements), expected)
-})
diff --git a/src/version-test.ts b/src/version-test.ts
new file mode 100644
index 0000000..163d506
--- /dev/null
+++ b/src/version-test.ts
@@ -0,0 +1,17 @@
+import test from 'ava'
+import { fromUrl } from './version'
+
+test('fromUrl()', (t) => {
+  t.is(
+    fromUrl('https://github.com/me/myproject/archive/v1.2.3.tar.gz'),
+    'v1.2.3'
+  )
+  t.is(
+    fromUrl(
+      'https://github.com/me/myproject/releases/download/v1.2.3/file.tgz'
+    ),
+    'v1.2.3'
+  )
+  t.is(fromUrl('http://myproject.net/download/v1.2.3.tgz'), 'v1.2.3')
+  t.is(fromUrl('https://example.com/v1.2.3.zip'), 'v1.2.3')
+})