diff --git a/README.md b/README.md index 0933c307..5bd2bdba 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ signAddon( // WebExtensions do not require an ID. // See the notes below about dealing with IDs. id: 'your-addon-id@somewhere', + // The release channel (listed or unlisted). + // Default: most recently used channel. + channel: undefined, // Save downloaded files to this directory. // Default: current working directory. downloadDir: undefined, diff --git a/src/amo-client.js b/src/amo-client.js index 407b72ee..f9ca68a4 100644 --- a/src/amo-client.js +++ b/src/amo-client.js @@ -77,12 +77,13 @@ export class Client { * - `xpiPath` Path to xpi file. * - `guid` Optional add-on GUID, aka the ID in install.rdf. * - `version` add-on version string. + * - `channel` release channel (listed, unlisted). * @return {Promise} signingResult with keys: * - success: boolean * - downloadedFiles: Array of file objects * - id: string identifier for the signed add-on */ - sign({guid, version, xpiPath}) { + sign({guid, version, channel=null, xpiPath}) { const formData = { upload: this._fs.createReadStream(xpiPath), @@ -93,6 +94,9 @@ export class Client { // PUT to a specific URL for this add-on + version. addonUrl += encodeURIComponent(guid) + "/versions/" + encodeURIComponent(version) + "/"; + if (channel) { + formData.channel = channel; + } } else { // POST to a generic URL to create a new add-on. this.debug("Signing add-on without an ID"); diff --git a/src/index.js b/src/index.js index bc7e52d1..da128ea4 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,9 @@ export default function signAddon( id, // The add-on version number for AMO. version, + // The release channel on AMO (listed or unlisted). + // Defaults to most recently used channel. + channel=null, // Your API key (JWT issuer) from AMO Devhub. apiKey, // Your API secret (JWT secret) from AMO Devhub. @@ -87,6 +90,7 @@ export default function signAddon( xpiPath: xpiPath, guid: id, version: version, + channel: channel, }); }); diff --git a/tests/test.amo-client.js b/tests/test.amo-client.js index e9aaa032..5c26890e 100644 --- a/tests/test.amo-client.js +++ b/tests/test.amo-client.js @@ -138,6 +138,8 @@ describe("amoClient.Client", function() { expect(putCall.conf.formData.upload).to.be.equal("fake-read-stream"); // When doing a PUT, the version is in the URL not the form data. expect(putCall.conf.formData.version).to.be.undefined; + // When no channel is supplied, the API is expected to use the most recent channel. + expect(putCall.conf.formData.channel).to.be.undefined; expect(waitForSignedAddon.called).to.be.equal(true); expect(waitForSignedAddon.firstCall.args[0]) @@ -171,6 +173,80 @@ describe("amoClient.Client", function() { expect(call.conf.url).to.match(/\/addons\/$/); expect(call.conf.formData.upload).to.be.equal("fake-read-stream"); expect(call.conf.formData.version).to.be.equal(conf.version); + // Channel is not a valid parameter for new add-ons. + expect(call.conf.formData.channel).to.be.undefined; + + expect(waitForSignedAddon.called).to.be.equal(true); + expect(waitForSignedAddon.firstCall.args[0]) + .to.be.equal(apiStatusUrl); + }); + }); + + it("lets you sign an unlisted add-on", function() { + var apiStatusUrl = "https://api/addon/version/upload/abc123"; + var conf = { + guid: "a-guid", + version: "a-version", + channel: "unlisted", + }; + var waitForSignedAddon = sinon.spy(() => {}); + this.client.waitForSignedAddon = waitForSignedAddon; + + this.client._request = new MockRequest({ + httpResponse: {statusCode: 202}, + // Partial response like: + // http://olympia.readthedocs.org/en/latest/topics/api/signing.html#checking-the-status-of-your-upload + responseBody: { + url: apiStatusUrl, + }, + }); + + return this.sign(conf).then(() => { + var putCall = this.client._request.calls[0]; + expect(putCall.name).to.be.equal("put"); + + var partialUrl = "/addons/" + conf.guid + "/versions/" + conf.version; + expect(putCall.conf.url).to.include(partialUrl); + expect(putCall.conf.formData.upload).to.be.equal("fake-read-stream"); + // When doing a PUT, the version is in the URL not the form data. + expect(putCall.conf.formData.version).to.be.undefined; + expect(putCall.conf.formData.channel).to.be.equal("unlisted"); + + expect(waitForSignedAddon.called).to.be.equal(true); + expect(waitForSignedAddon.firstCall.args[0]) + .to.be.equal(apiStatusUrl); + }); + }); + + it("lets you sign a listed add-on", function() { + var apiStatusUrl = "https://api/addon/version/upload/abc123"; + var conf = { + guid: "a-guid", + version: "a-version", + channel: "listed", + }; + var waitForSignedAddon = sinon.spy(() => {}); + this.client.waitForSignedAddon = waitForSignedAddon; + + this.client._request = new MockRequest({ + httpResponse: {statusCode: 202}, + // Partial response like: + // http://olympia.readthedocs.org/en/latest/topics/api/signing.html#checking-the-status-of-your-upload + responseBody: { + url: apiStatusUrl, + }, + }); + + return this.sign(conf).then(() => { + var putCall = this.client._request.calls[0]; + expect(putCall.name).to.be.equal("put"); + + var partialUrl = "/addons/" + conf.guid + "/versions/" + conf.version; + expect(putCall.conf.url).to.include(partialUrl); + expect(putCall.conf.formData.upload).to.be.equal("fake-read-stream"); + // When doing a PUT, the version is in the URL not the form data. + expect(putCall.conf.formData.version).to.be.undefined; + expect(putCall.conf.formData.channel).to.be.equal("listed"); expect(waitForSignedAddon.called).to.be.equal(true); expect(waitForSignedAddon.firstCall.args[0]) diff --git a/tests/test.sign.js b/tests/test.sign.js index 46fcb88f..a05649fd 100644 --- a/tests/test.sign.js +++ b/tests/test.sign.js @@ -97,6 +97,16 @@ describe("sign", function() { }); }); + it("passes release channel to the signer", () => { + const channel = "listed"; + return runSignCmd({ + cmdOptions: {channel}, + }).then(function() { + expect(signingCall.called).to.be.equal(true); + expect(signingCall.firstCall.args[0].channel).to.be.equal(channel); + }); + }); + it("passes JWT expiration to the signing client", () => { const expiresIn = 60 * 15; // 15 minutes return runSignCmd({