Skip to content

Commit

Permalink
Merge pull request #9 from erikdesjardins/poll
Browse files Browse the repository at this point in the history
Polling for validation results
  • Loading branch information
erikdesjardins committed Feb 13, 2017
2 parents 7412daf + caf7a27 commit fb7c893
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 21 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -17,7 +17,7 @@ var deploy = require('firefox-extension-deploy');

deploy({
// obtained by following the instructions here:
// https://olympia.readthedocs.io/en/latest/topics/api/auth.html
// https://addons-server.readthedocs.io/en/latest/topics/api/auth.html
// or from this page:
// https://addons.mozilla.org/en-US/developers/addon/api/key/
issuer: 'myIssuer',
Expand Down
42 changes: 34 additions & 8 deletions index.js
Expand Up @@ -10,13 +10,21 @@ var jwt = require('jsonwebtoken');

var REQUIRED_FIELDS = ['issuer', 'secret', 'id', 'version', 'src'];

function sleep(ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms);
});
}

module.exports = function deploy(options) {
var jwtIssuer = options.issuer;
var jwtSecret = options.secret;
var extensionId = options.id;
var extensionVersion = options.version;
var srcFile = options.src;

var token, uploadId;

return Promise.resolve()
// options validation
.then(function() {
Expand All @@ -26,30 +34,48 @@ module.exports = function deploy(options) {
}
});
})
// submit the addon
// prepare token
.then(function() {
var issuedAt = Math.floor(Date.now() / 1000);
var payload = {
iss: jwtIssuer,
jti: Math.random().toString(),
iat: issuedAt,
exp: issuedAt + 60
exp: issuedAt + 300
};
var token = jwt.sign(payload, jwtSecret, { algorithm: 'HS256' });

token = jwt.sign(payload, jwtSecret, { algorithm: 'HS256' });
})
// submit the addon
.then(function() {
return request
.put('https://addons.mozilla.org/api/v3/addons/' + extensionId + '/versions/' + extensionVersion + '/')
.set('Authorization', 'JWT ' + token)
.field('upload', srcFile)
.then(function() {
// success
.then(function(response) {
uploadId = response.body.pk;
}, function(err) {
switch (err.response.status) {
case 401:
throw new Error('401 Unauthorized: ' + err.response.body.detail);
throw new Error('Submission failed: 401 Unauthorized: ' + err.response.body.detail);
default:
throw new Error('Status ' + err.response.status + ': ' + err.response.body.error);
throw new Error('Submission failed: Status ' + err.response.status + ': ' + err.response.body.error);
}
});
})
// poll for completion
.then(function poll() {
return request
.get('https://addons.mozilla.org/api/v3/addons/' + extensionId + '/versions/' + extensionVersion + '/uploads/' + uploadId + '/')
.set('Authorization', 'JWT ' + token)
.then(function(response) {
if (!response.body.processed) {
// try again
return sleep(30000).then(poll);
} else if (!response.body.valid) {
throw new Error('Validation failed: ' + response.body.validation_url + ' ' + JSON.stringify(response.body.validation_results));
}
}, function(err) {
throw new Error('Polling failed: Status ' + err.response.status + ': ' + err.response.body.error);
})
});
};
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "firefox-extension-deploy",
"version": "1.0.0",
"version": "1.1.0",
"description": "Deploy Firefox extensions to AMO.",
"main": "index.js",
"scripts": {
Expand Down
93 changes: 82 additions & 11 deletions test/test.js
Expand Up @@ -8,7 +8,7 @@ import deploy from '../index.js';
test.beforeEach(t => {
t.context.requests = [];
t.context.mock = superagentMock(superagent, [{
pattern: '^https://addons.mozilla.org/api/v3/addons/(.+)/versions/(.+)/$',
pattern: '^https://addons.mozilla.org/api/v3/addons/([^/]+)/versions/([^/]+)/$',
fixtures(match, params, headers) {
t.context.requests.push({ match, params, headers });
if (t.context.publishFail) {
Expand All @@ -19,6 +19,18 @@ test.beforeEach(t => {
put(match, data) {
return { body: data };
}
}, {
pattern: '^https://addons.mozilla.org/api/v3/addons/([^/]+)/versions/([^/]+)/uploads/([^/]+)/$',
fixtures(match, params, headers) {
t.context.requests.push({ match, params, headers });
if (t.context.validationFail) {
throw { response: { status: t.context.validationFail, body: {} } };
}
return t.context.validationResponse || t.context.validationResponses.shift();
},
get(match, data) {
return { body: data };
}
}, {
pattern: '.*',
fixtures(match) {
Expand Down Expand Up @@ -59,7 +71,7 @@ test.serial('failing upload, unknown status', async t => {

await t.throws(
deploy({ issuer: 'q', secret: 'q', id: 'q', version: 'q', src: 'q' }),
'Status fail_message: undefined'
'Submission failed: Status fail_message: undefined'
);
});

Expand All @@ -68,7 +80,7 @@ test.serial('failing upload, 400', async t => {

await t.throws(
deploy({ issuer: 'q', secret: 'q', id: 'q', version: 'q', src: 'q' }),
'Status 400: undefined'
'Submission failed: Status 400: undefined'
);
});

Expand All @@ -77,7 +89,7 @@ test.serial('failing upload, 401', async t => {

await t.throws(
deploy({ issuer: 'q', secret: 'q', id: 'q', version: 'q', src: 'q' }),
'401 Unauthorized: undefined'
'Submission failed: 401 Unauthorized: undefined'
);
});

Expand All @@ -86,7 +98,7 @@ test.serial('failing upload, 403', async t => {

await t.throws(
deploy({ issuer: 'q', secret: 'q', id: 'q', version: 'q', src: 'q' }),
'Status 403: undefined'
'Submission failed: Status 403: undefined'
);
});

Expand All @@ -95,24 +107,83 @@ test.serial('failing upload, 409', async t => {

await t.throws(
deploy({ issuer: 'q', secret: 'q', id: 'q', version: 'myVersion', src: 'q' }),
'Status 409: undefined'
'Submission failed: Status 409: undefined'
);
});

test.serial('full deploy', async t => {
test.serial('failing polling, 409', async t => {
t.context.publishResponse = {};
t.context.validationFail = 409;

await t.throws(
deploy({ issuer: 'q', secret: 'q', id: 'q', version: 'myVersion', src: 'q' }),
'Polling failed: Status 409: undefined'
);
});

test.serial('failing validation', async t => {
t.context.publishResponse = { pk: 'somePk' };
t.context.validationResponse = { processed: true, valid: false, validation_url: 'myUrl', validation_results: 'myResults' };

await t.throws(
deploy({ issuer: 'q', secret: 'q', id: 'q', version: 'myVersion', src: 'q' }),
'Validation failed: myUrl "myResults"'
);

t.is(t.context.requests.length, 2);
});

test.serial('full deploy', async t => {
t.context.publishResponse = { pk: 'somePk' };
t.context.validationResponse = { processed: true, valid: true };

await deploy({ issuer: 'someIssuer', secret: 'someSecret', id: 'someId', version: 'someVersion', src: 'someSrc' })

const { requests: [publishReq] } = t.context;
t.is(t.context.requests.length, 2, 'only two requests made');

const { requests: [publishReq, validationReq] } = t.context;

t.is(publishReq.match[1], 'someId');
t.is(publishReq.match[2], 'someVersion');
t.regex(publishReq.headers['Authorization'], /^JWT /);

// throws if invalid
jwt.verify(publishReq.headers['Authorization'].slice(4), 'someSecret', {
algorithms: ['HS256'],
issuer: 'someIssuer'
})
});

t.is(validationReq.match[1], 'someId');
t.is(validationReq.match[2], 'someVersion');
t.is(validationReq.match[3], 'somePk');
t.regex(validationReq.headers['Authorization'], /^JWT /);
jwt.verify(validationReq.headers['Authorization'].slice(4), 'someSecret', {
algorithms: ['HS256'],
issuer: 'someIssuer'
});
});

test.serial('failing validation after polling', async t => {
t.context.publishResponse = { pk: 'somePk' };
t.context.validationResponses = [
{ processed: false },
{ processed: true, valid: false, validation_url: 'myUrl', validation_results: 'myResults' },
];

await t.throws(
deploy({ issuer: 'q', secret: 'q', id: 'q', version: 'myVersion', src: 'q' }),
'Validation failed: myUrl "myResults"'
);

t.is(t.context.requests.length, 3);
});

test.serial('passing validation after polling', async t => {
t.context.publishResponse = { pk: 'somePk' };
t.context.validationResponses = [
{ processed: false },
{ processed: true, valid: true },
];

await deploy({ issuer: 'q', secret: 'q', id: 'q', version: 'myVersion', src: 'q' });

t.is(t.context.requests.length, 3);
});

0 comments on commit fb7c893

Please sign in to comment.