diff --git a/src/cmd/sign.js b/src/cmd/sign.js index 15d88d426d..3f6aa67920 100644 --- a/src/cmd/sign.js +++ b/src/cmd/sign.js @@ -41,6 +41,7 @@ export default function sign( channel, amoMetadata, uploadSourceCode, + amoIcon, webextVersion, }, { @@ -154,6 +155,7 @@ export default function sign( approvalCheckTimeout: approvalTimeout !== undefined ? approvalTimeout : timeout, submissionSource: uploadSourceCode, + amoIcon, }); } else { const { diff --git a/src/program.js b/src/program.js index 50a6b5f7b1..c99c53fe6a 100644 --- a/src/program.js +++ b/src/program.js @@ -609,6 +609,13 @@ Example: $0 --help run. 'details. Only used with `use-submission-api`', type: 'string', }, + 'amo-icon': { + describe: + 'Path to an image that should be displayed as the addon icon on ' + + 'addons.mozilla.org. Must be square; 128px x 128px is recommended. ' + + 'Only used with `use-submission-api`', + type: 'string', + }, }, ) .command('run', 'Run the extension', commands.run, { diff --git a/src/util/submit-addon.js b/src/util/submit-addon.js index 160cbf9dbf..5afb614910 100644 --- a/src/util/submit-addon.js +++ b/src/util/submit-addon.js @@ -186,10 +186,11 @@ export default class Client { } async doFormDataPatch(data, addonId, versionId) { - const patchUrl = new URL( - `addon/${addonId}/versions/${versionId}/`, - this.apiUrl, - ); + let patchUrl = new URL(`addon/${addonId}/`, this.apiUrl); + if (versionId) { + patchUrl = new URL(`versions/${versionId}/`, patchUrl); + } + try { const formData = new FormData(); for (const field in data) { @@ -207,10 +208,19 @@ export default class Client { } async doAfterSubmit(addonId, newVersionId, editUrl, patchData) { + const promises = []; if (patchData && patchData.version) { log.info(`Submitting ${Object.keys(patchData.version)} to version`); - await this.doFormDataPatch(patchData.version, addonId, newVersionId); + promises.push( + this.doFormDataPatch(patchData.version, addonId, newVersionId), + ); } + if (patchData && patchData.addon) { + log.info(`Submitting ${Object.keys(patchData.addon)} to addon`); + promises.push(this.doFormDataPatch(patchData.addon, addonId)); + } + await Promise.all(promises); + if (this.approvalCheckTimeout > 0) { const fileUrl = new URL( await this.waitForApproval(addonId, newVersionId), @@ -415,19 +425,16 @@ export async function signAddon({ savedUploadUuidPath, metaDataJson = {}, submissionSource, + amoIcon, userAgentString, SubmitClient = Client, ApiAuthClass = JwtApiAuth, }) { - try { - const stats = await fsPromises.stat(xpiPath); - - if (!stats.isFile()) { - throw new Error('not a file'); - } - } catch (statError) { - throw new Error(`error with ${xpiPath}: ${statError}`); - } + await Promise.all([ + checkPathIsFile(xpiPath), + submissionSource ? checkPathIsFile(submissionSource) : Promise.resolve(), + amoIcon ? checkPathIsFile(amoIcon) : Promise.resolve(), + ]); let baseUrl; try { @@ -451,19 +458,13 @@ export async function signAddon({ savedUploadUuidPath, ); const patchData = {}; - // if we have a source file we need to upload we patch after the create + // if we have a source or icon file we need to upload we patch after the create if (submissionSource) { - try { - const stats2 = await fsPromises.stat(submissionSource); - - if (!stats2.isFile()) { - throw new Error('not a file'); - } - } catch (statError) { - throw new Error(`error with ${submissionSource}: ${statError}`); - } patchData.version = { source: client.fileFromSync(submissionSource) }; } + if (amoIcon) { + patchData.addon = { icon: client.fileFromSync(amoIcon) }; + } // We specifically need to know if `id` has not been passed as a parameter because // it's the indication that a new add-on should be created, rather than a new version. @@ -526,3 +527,15 @@ export async function getUploadUuidFromFile( return { uploadUuid: '', channel: '', xpiCrcHash: '' }; } + +async function checkPathIsFile(filePath) { + try { + const stats2 = await fsPromises.stat(filePath); + + if (!stats2.isFile()) { + throw new Error('not a file'); + } + } catch (statError) { + throw new Error(`error with ${filePath}: ${statError}`); + } +} diff --git a/tests/unit/test-util/test.submit-addon.js b/tests/unit/test-util/test.submit-addon.js index 70850722ea..dd732a862d 100644 --- a/tests/unit/test-util/test.submit-addon.js +++ b/tests/unit/test-util/test.submit-addon.js @@ -235,6 +235,44 @@ describe('util.submit-addon', () => { ); }); + it('includes icon data to be patched if amoIcon defined for new addon', async () => { + const amoIcon = 'path/to/icon/image'; + statStub.onSecondCall().resolves({ isFile: () => true }); + await signAddon({ + ...signAddonDefaults, + amoIcon, + }); + + sinon.assert.calledWith(fileFromSyncStub, amoIcon); + sinon.assert.calledWith( + postNewAddonStub, + uploadUuid, + signAddonDefaults.savedIdPath, + {}, + { addon: { icon: fakeFileFromSync } }, + ); + }); + + it('includes icon data to be patched if amoIcon defined for new version', async () => { + const amoIcon = 'path/to/icon/image'; + statStub.onSecondCall().resolves({ isFile: () => true }); + const id = '@thisID'; + await signAddon({ + ...signAddonDefaults, + amoIcon, + id, + }); + + sinon.assert.calledWith(fileFromSyncStub, amoIcon); + sinon.assert.calledWith( + putVersionStub, + uploadUuid, + id, + {}, + { addon: { icon: fakeFileFromSync } }, + ); + }); + it('throws error if submissionSource is not found', async () => { const submissionSource = 'path/to/source/zip'; const signAddonPromise = signAddon({ @@ -1041,7 +1079,6 @@ describe('util.submit-addon', () => { const downloadUrl = 'https://a.download/url'; const newVersionId = sampleVersionDetail.id; const editUrl = sampleVersionDetail.editUrl; - const patchData = { version: { source: 'somesource' } }; let approvalStub; let downloadStub; @@ -1079,8 +1116,45 @@ describe('util.submit-addon', () => { it('calls doFormDataPatch if patchData.version is defined', async () => { client.approvalCheckTimeout = 0; + const patchData = { version: { source: 'somesource' } }; + await client.doAfterSubmit(addonId, newVersionId, editUrl, patchData); + + sinon.assert.calledOnce(doFormDataPatchStub); + sinon.assert.calledWith( + doFormDataPatchStub, + patchData.version, + addonId, + newVersionId, + ); + }); + + it('calls doFormDataPatch if patchData.addon is defined', async () => { + client.approvalCheckTimeout = 0; + const patchData = { addon: { icon: 'someimage' } }; await client.doAfterSubmit(addonId, newVersionId, editUrl, patchData); + sinon.assert.calledOnce(doFormDataPatchStub); + sinon.assert.calledWith( + doFormDataPatchStub, + patchData.addon, + addonId, + ); + }); + + it('calls doFormDataPatch twice if patchData.addon and patchData.version is defined', async () => { + client.approvalCheckTimeout = 0; + const patchData = { + version: { source: 'somesource' }, + addon: { icon: 'someimage' }, + }; + await client.doAfterSubmit(addonId, newVersionId, editUrl, patchData); + + sinon.assert.callCount(doFormDataPatchStub, 2); + sinon.assert.calledWith( + doFormDataPatchStub, + patchData.addon, + addonId, + ); sinon.assert.calledWith( doFormDataPatchStub, patchData.version,