From da4a3628d2fe641b89d0422a91e964f73e5f4542 Mon Sep 17 00:00:00 2001
From: Emmanuel Ogbizi <iamogbz+github@gmail.com>
Date: Tue, 17 Mar 2020 06:03:04 -0400
Subject: [PATCH] fix: exit successfully when submitting a listed add-on (#91)

* chore: add allowed channels constants

* chore: tests

* test: wip

* test: wip use union fs

* test: wip use memfs

* test: wip

* chore: use memfs

* test: move mock to setup

* chore: ensure caveat only applies for listed and autosign

* chore: only check if folder exists
---
 package.json                  |  4 +-
 src/constants.js              |  8 +++-
 src/publish.js                | 25 +++++++-----
 tests/__mocks__/fs.js         | 21 +++++++++-
 tests/__mocks__/sign-addon.js |  1 +
 tests/__mocks__/web-ext.js    |  6 ---
 tests/prepare.test.js         |  4 +-
 tests/publish.test.js         | 72 +++++++++++++++++++++++++++-----
 tests/setup.js                |  2 +-
 yarn.lock                     | 77 +++++++++++------------------------
 10 files changed, 132 insertions(+), 88 deletions(-)
 create mode 100644 tests/__mocks__/sign-addon.js
 delete mode 100644 tests/__mocks__/web-ext.js

diff --git a/package.json b/package.json
index a2fd6f1..95de237 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
     "@tophat/commitizen-adapter": "^0.0.11",
     "@tophat/commitlint-config": "^0.1.2",
     "@tophat/eslint-config": "^0.6.0",
+    "@types/jest": "^25.1.4",
     "aggregate-error": "^3.0.1",
     "all-contributors-cli": "^6.9.1",
     "babel-eslint": "^10.0.2",
@@ -38,14 +39,15 @@
     "memfs": "^3.0.3",
     "prettier": "^1.18.2",
     "semantic-release": "^17.0.1",
+    "unionfs": "^4.4.0",
     "yarn-deduplicate": "^2.0.0"
   },
   "scripts": {
     "build": "mkdir -p artifacts; echo 'When changing this remember to update @semantic-release/npm.'",
     "commit": "git-cz",
     "lock-check": "yarn-deduplicate --list --fail",
-    "lock-dedup": "yarn-deduplicate",
     "lint": "eslint . --ext .js,.ts --max-warnings=0",
+    "postinstall": "yarn-deduplicate",
     "release": "semantic-release",
     "report-coverage": "codecov",
     "test": "jest",
diff --git a/src/constants.js b/src/constants.js
index 2cc1e7c..c5dea36 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -1,6 +1,11 @@
+const allowedChannels = {
+    LISTED: 'listed',
+    UNLISTED: 'unlisted',
+}
+
 const defaultOptions = {
     artifactsDir: './artifacts',
-    channel: 'unlisted',
+    channel: allowedChannels.UNLISTED,
     manifestPath: 'manifest.json',
     sourceDir: 'dist',
 }
@@ -18,6 +23,7 @@ const requiredEnvs = {
 }
 
 module.exports = {
+    allowedChannels,
     defaultOptions,
     requiredEnvs,
     requiredOptions,
diff --git a/src/publish.js b/src/publish.js
index d388872..56ae033 100644
--- a/src/publish.js
+++ b/src/publish.js
@@ -4,6 +4,7 @@ const path = require('path')
 const webExt = require('web-ext').default
 const defaultAddonSigner = require('sign-addon')
 
+const { allowedChannels } = require('./constants')
 const { verifyOptions } = require('./utils')
 
 const publish = async options => {
@@ -24,18 +25,25 @@ const publish = async options => {
 
     const { FIREFOX_API_KEY, FIREFOX_SECRET_KEY } = process.env
 
-    let unsignedXpiPath
     const signAddon = async params => {
-        unsignedXpiPath = params.xpiPath
+        const unsignedXpiFile = `unsigned-${targetXpi}`
+        fs.writeFileSync(
+            path.join(artifactsDir, unsignedXpiFile),
+            fs.readFileSync(params.xpiPath),
+        )
         const result = await defaultAddonSigner(params)
-        if (!result.success && result.errorCode === 'ADDON_NOT_AUTO_SIGNED') {
+        if (
+            channel === allowedChannels.LISTED &&
+            !result.success &&
+            result.errorCode === 'ADDON_NOT_AUTO_SIGNED'
+        ) {
             result.success = true
-            result.downloadedFiles = result.downloadedFiles || []
+            result.downloadedFiles = result.downloadedFiles || [unsignedXpiFile]
         }
         return result
     }
 
-    const { success, downloadedFiles } = await webExt.cmd.sign(
+    const { downloadedFiles } = await webExt.cmd.sign(
         {
             apiKey: FIREFOX_API_KEY,
             apiSecret: FIREFOX_SECRET_KEY,
@@ -46,14 +54,9 @@ const publish = async options => {
         },
         { signAddon },
     )
-    if (!success) {
-        throw new Error(
-            'Signing the extension failed. See the console output from web-ext sign for the validation link',
-        )
-    }
     const [xpiFile] = downloadedFiles
     fs.renameSync(
-        xpiFile ? path.join(artifactsDir, xpiFile) : unsignedXpiPath,
+        path.join(artifactsDir, xpiFile),
         path.join(artifactsDir, targetXpi),
     )
 }
diff --git a/tests/__mocks__/fs.js b/tests/__mocks__/fs.js
index e5f40ad..19205a6 100644
--- a/tests/__mocks__/fs.js
+++ b/tests/__mocks__/fs.js
@@ -1,3 +1,20 @@
-const { fs } = require('memfs')
+const path = jest.requireActual('path')
+const fs = jest.requireActual('fs')
+const { vol } = require('memfs')
+const { ufs } = require('unionfs')
 
-module.exports = fs
+const { createWriteStream } = ufs
+ufs.createWriteStream = (...args) => {
+    for (const _fs of ufs.fss) {
+        try {
+            if (_fs.existsSync(path.dirname(`${args[0]}`))) {
+                return _fs.createWriteStream(args[0])
+            }
+        } catch (e) {
+            continue
+        }
+    }
+    return createWriteStream(...args)
+}
+
+module.exports = ufs.use(vol).use(fs)
diff --git a/tests/__mocks__/sign-addon.js b/tests/__mocks__/sign-addon.js
new file mode 100644
index 0000000..b4bbacb
--- /dev/null
+++ b/tests/__mocks__/sign-addon.js
@@ -0,0 +1 @@
+module.exports = jest.fn()
diff --git a/tests/__mocks__/web-ext.js b/tests/__mocks__/web-ext.js
deleted file mode 100644
index 8319aef..0000000
--- a/tests/__mocks__/web-ext.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const mockWebExt = {
-    cmd: {
-        sign: jest.fn(),
-    },
-}
-module.exports = { default: mockWebExt }
diff --git a/tests/prepare.test.js b/tests/prepare.test.js
index bb54ffa..200d6cc 100644
--- a/tests/prepare.test.js
+++ b/tests/prepare.test.js
@@ -1,4 +1,6 @@
-const { fs, vol } = require('memfs')
+const fs = require('fs')
+
+const { vol } = require('memfs')
 
 const { prepare } = require('../src')
 
diff --git a/tests/publish.test.js b/tests/publish.test.js
index 66c9ee3..2af8548 100644
--- a/tests/publish.test.js
+++ b/tests/publish.test.js
@@ -1,23 +1,49 @@
-const { fs, vol } = require('memfs')
-const { default: webExt } = require('web-ext')
+const fs = require('fs')
+const path = require('path')
+
+const { vol } = require('memfs')
+const signAddon = require('sign-addon')
 
 const { publish } = require('../src')
 
 describe('publish', () => {
+    const mockManifestJSON = {
+        manifest_version: 2,
+        name: 'Mock Extension',
+        version: '0.0.1',
+    }
     const extensionId = '{01234567-abcd-6789-cdef-0123456789ef}'
     const targetXpi = 'target-extension.xpi'
     const mockOptions = {
         artifactsDir: 'mock_artifacts',
-        manifestPath: 'mock_manifest.json',
+        channel: 'unlisted',
+        manifestPath: 'manifest.json',
         sourceDir: 'mock_source',
     }
     const completeOptions = { extensionId, targetXpi, ...mockOptions }
+    const mockAddonSignFailed = { success: false }
+    const mockAddonSignSuccess = { success: true, id: extensionId }
+    const clearMockArtifacts = () => {
+        const actualFs = jest.requireActual('fs')
+        if (actualFs.existsSync(mockOptions.artifactsDir)) {
+            actualFs.rmdirSync(mockOptions.artifactsDir, { recursive: true })
+        }
+    }
 
     beforeAll(() => {
         jest.spyOn(console, 'log')
     })
+    beforeEach(() => {
+        vol.fromJSON({
+            [path.join(
+                mockOptions.sourceDir,
+                mockOptions.manifestPath,
+            )]: JSON.stringify(mockManifestJSON),
+        })
+    })
     afterEach(() => {
         vol.reset()
+        clearMockArtifacts()
         jest.clearAllMocks()
     })
     afterAll(() => {
@@ -36,25 +62,49 @@ describe('publish', () => {
         )
     })
 
-    it('raises error if signing unsuccessful', () => {
-        webExt.cmd.sign.mockResolvedValueOnce({ success: false })
+    it.each`
+        signCase                                               | signResults
+        ${'signing unsuccessful'}                              | ${mockAddonSignFailed}
+        ${'auto signing unsuccessful and channel is unlisted'} | ${{ ...mockAddonSignFailed, errorCode: 'ADDON_NOT_AUTO_SIGNED' }}
+    `('raises error if $signCase', ({ signResults }) => {
+        signAddon.mockResolvedValueOnce(signResults)
         return expect(publish(completeOptions)).rejects.toThrow(
-            'Signing the extension failed',
+            'The extension could not be signed',
         )
     })
 
+    it('uses unsigned xpi if auto signing unsuccessful and channel is listed', async () => {
+        signAddon.mockResolvedValueOnce({
+            ...mockAddonSignFailed,
+            errorCode: 'ADDON_NOT_AUTO_SIGNED',
+        })
+        const targetXpiPath = path.join(mockOptions.artifactsDir, targetXpi)
+        expect(fs.existsSync(targetXpiPath)).toBe(false)
+        await publish({
+            ...completeOptions,
+            channel: 'listed',
+        })
+        expect(fs.existsSync(targetXpiPath)).toBe(true)
+    })
+
     it('renames downloaded file to target xpi', async () => {
         const downloadedFile = 'mock_downloaded.xpi'
+        const mockFileContent = 'some fake signed xpi'
         vol.fromJSON({
-            [`${mockOptions.artifactsDir}/${downloadedFile}`]: 'some fake signed xpi',
+            [path.join(
+                mockOptions.artifactsDir,
+                downloadedFile,
+            )]: mockFileContent,
         })
-        webExt.cmd.sign.mockResolvedValueOnce({
-            success: true,
+        signAddon.mockResolvedValueOnce({
+            ...mockAddonSignSuccess,
             downloadedFiles: [downloadedFile],
         })
-        const targetXpiPath = `${mockOptions.artifactsDir}/${targetXpi}`
+        const targetXpiPath = path.join(mockOptions.artifactsDir, targetXpi)
         expect(fs.existsSync(targetXpiPath)).toBe(false)
         await publish(completeOptions)
-        expect(fs.existsSync(targetXpiPath)).toBe(true)
+        expect(fs.readFileSync(targetXpiPath).toString()).toEqual(
+            mockFileContent,
+        )
     })
 })
diff --git a/tests/setup.js b/tests/setup.js
index 54bed39..57de7c0 100644
--- a/tests/setup.js
+++ b/tests/setup.js
@@ -1,2 +1,2 @@
 jest.mock('fs')
-jest.mock('web-ext')
+jest.mock('sign-addon')
diff --git a/yarn.lock b/yarn.lock
index dca3ad2..211b579 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -188,17 +188,7 @@
     "@babel/traverse" "^7.8.3"
     "@babel/types" "^7.8.3"
 
-"@babel/helper-replace-supers@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.3.tgz#91192d25f6abbcd41da8a989d4492574fb1530bc"
-  integrity sha512-xOUssL6ho41U81etpLoT2RTdvdus4VfHamCuAm4AHxGr+0it5fnwoVdwUJ7GFEqCsQYzJUhcbsN9wB9apcYKFA==
-  dependencies:
-    "@babel/helper-member-expression-to-functions" "^7.8.3"
-    "@babel/helper-optimise-call-expression" "^7.8.3"
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
-
-"@babel/helper-replace-supers@^7.8.6":
+"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6":
   version "7.8.6"
   resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8"
   integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==
@@ -715,14 +705,7 @@
   dependencies:
     regenerator-runtime "^0.13.2"
 
-"@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6":
-  version "7.8.4"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308"
-  integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==
-  dependencies:
-    regenerator-runtime "^0.13.2"
-
-"@babel/runtime@^7.8.4":
+"@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4":
   version "7.8.7"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.7.tgz#8fefce9802db54881ba59f90bb28719b4996324d"
   integrity sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg==
@@ -845,20 +828,7 @@
     babel-runtime "^6.23.0"
     lodash "4.17.15"
 
-"@commitlint/load@>6.1.1":
-  version "8.3.4"
-  resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-8.3.4.tgz#6a0832362451b959f6aa47da8e44c2e05572b114"
-  integrity sha512-B4MylKvT02UE3VHC5098OHxsrgkADUy5AD4Cdkiy7oX/edWypEmvK7Wuns3B9dwluWP/iFM6daoWtpkCVZoRwQ==
-  dependencies:
-    "@commitlint/execute-rule" "^8.3.4"
-    "@commitlint/resolve-extends" "^8.3.4"
-    babel-runtime "^6.23.0"
-    chalk "2.4.2"
-    cosmiconfig "^5.2.0"
-    lodash "4.17.15"
-    resolve-from "^5.0.0"
-
-"@commitlint/load@^8.3.5":
+"@commitlint/load@>6.1.1", "@commitlint/load@^8.3.5":
   version "8.3.5"
   resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-8.3.5.tgz#3f059225ede92166ba94cf4c48e3d67c8b08b18a"
   integrity sha512-poF7R1CtQvIXRmVIe63FjSQmN9KDqjRtU5A6hxqXBga87yB2VUJzic85TV6PcQc+wStk52cjrMI+g0zFx+Zxrw==
@@ -895,17 +865,6 @@
     babel-runtime "^6.23.0"
     git-raw-commits "^2.0.0"
 
-"@commitlint/resolve-extends@^8.3.4":
-  version "8.3.4"
-  resolved "https://registry.yarnpkg.com/@commitlint/resolve-extends/-/resolve-extends-8.3.4.tgz#815b646efbf9bc77c44925f619336da0027d7a68"
-  integrity sha512-M34RLaAW1eGWgtkVtotHfPaJa+cZIARe8twKItd7RhWs7n/1W2py9GTFIiIEq95LBN1uah5vm1WQHsfLqPZYHA==
-  dependencies:
-    "@types/node" "^12.0.2"
-    import-fresh "^3.0.0"
-    lodash "4.17.15"
-    resolve-from "^5.0.0"
-    resolve-global "^1.0.0"
-
 "@commitlint/resolve-extends@^8.3.5":
   version "8.3.5"
   resolved "https://registry.yarnpkg.com/@commitlint/resolve-extends/-/resolve-extends-8.3.5.tgz#8fff800f292ac217ae30b1862f5f9a84b278310a"
@@ -1392,6 +1351,14 @@
     "@types/istanbul-lib-coverage" "*"
     "@types/istanbul-lib-report" "*"
 
+"@types/jest@^25.1.4":
+  version "25.1.4"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.1.4.tgz#9e9f1e59dda86d3fd56afce71d1ea1b331f6f760"
+  integrity sha512-QDDY2uNAhCV7TMCITrxz+MRk1EizcsevzfeS6LykIlq2V1E5oO4wXG8V2ZEd9w7Snxeeagk46YbMgZ8ESHx3sw==
+  dependencies:
+    jest-diff "^25.1.0"
+    pretty-format "^25.1.0"
+
 "@types/json-schema@^7.0.3":
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
@@ -1402,7 +1369,7 @@
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
 
-"@types/node@*", "@types/node@^12.0.2":
+"@types/node@*":
   version "12.7.4"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.4.tgz#64db61e0359eb5a8d99b55e05c729f130a678b04"
   integrity sha512-W0+n1Y+gK/8G2P/piTkBBN38Qc5Q1ZSO6B5H3QmPCUewaiXOo2GCAWZ4ElZCcNhjJuBSUSLGFUJnmlCn5+nxOQ==
@@ -4610,7 +4577,7 @@ fs-minipass@^1.2.5:
   dependencies:
     minipass "^2.6.0"
 
-fs-monkey@1.0.0:
+fs-monkey@1.0.0, fs-monkey@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.0.tgz#b1fe36b2d8a78463fd0b8fd1463b355952743bd0"
   integrity sha512-nxkkzQ5Ga+ETriXxIof4TncyMSzrV9jFIF+kGN16nw5CiAdWAnG/2FgM7CHhRenW1EBiDx+r1tf/P78HGKCgnA==
@@ -8421,16 +8388,11 @@ performance-now@^2.1.0:
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
-picomatch@^2.0.4:
+picomatch@^2.0.4, picomatch@^2.0.5:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a"
   integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==
 
-picomatch@^2.0.5:
-  version "2.0.7"
-  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6"
-  integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==
-
 pify@^2.0.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -8938,7 +8900,7 @@ regenerate@^1.4.0:
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
   integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
 
-regenerator-runtime@0.13.3, regenerator-runtime@^0.13.2:
+regenerator-runtime@0.13.3:
   version "0.13.3"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
   integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==
@@ -8953,7 +8915,7 @@ regenerator-runtime@^0.11.0:
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
   integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
 
-regenerator-runtime@^0.13.4:
+regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4:
   version "0.13.4"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz#e96bf612a3362d12bb69f7e8f74ffeab25c7ac91"
   integrity sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g==
@@ -10543,6 +10505,13 @@ union-value@^1.0.0:
     is-extendable "^0.1.1"
     set-value "^2.0.1"
 
+unionfs@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/unionfs/-/unionfs-4.4.0.tgz#b671112e505f70678052345cf5c2a33f0e6edde9"
+  integrity sha512-N+TuJHJ3PjmzIRCE1d2N3VN4qg/P78eh/nxzwHnzpg3W2Mvf8Wvi7J1mvv6eNkb8neUeSdFSQsKna0eXVyF4+w==
+  dependencies:
+    fs-monkey "^1.0.0"
+
 unique-filename@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"