Skip to content

Commit df6d215

Browse files
committed
implement source file upload
1 parent 0d090ae commit df6d215

File tree

5 files changed

+241
-5
lines changed

5 files changed

+241
-5
lines changed

src/cmd/sign.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default function sign(
4040
verbose,
4141
channel,
4242
amoMetadata,
43+
versionSource,
4344
webextVersion,
4445
},
4546
{
@@ -152,6 +153,7 @@ export default function sign(
152153
validationCheckTimeout: timeout,
153154
approvalCheckTimeout:
154155
approvalTimeout !== undefined ? approvalTimeout : timeout,
156+
versionSource,
155157
});
156158
} else {
157159
const {

src/program.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,11 @@ Example: $0 --help run.
601601
'Only used with `use-submission-api`',
602602
type: 'string',
603603
},
604+
'version-source': {
605+
describe:
606+
'Path to a zip file containing human readable source code for a version. ' +
607+
'Only used with `use-submission-api`',
608+
},
604609
},
605610
)
606611
.command('run', 'Run the extension', commands.run, {

src/util/submit-addon.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,27 @@ export default class Client {
185185
return this.fetchJson(url, 'PUT', JSON.stringify(jsonData));
186186
}
187187

188+
async doFormDataPatch(data, addonId, versionId) {
189+
const patchUrl = new URL(
190+
`addon/${addonId}/versions/${versionId}/`,
191+
this.apiUrl,
192+
);
193+
try {
194+
const formData = new FormData();
195+
for (const field in data) {
196+
formData.set(field, data[field]);
197+
}
198+
199+
const response = await this.fetch(patchUrl, 'PATCH', formData);
200+
if (!response.ok) {
201+
throw new Error(`response status was ${response.status}`);
202+
}
203+
} catch (error) {
204+
log.info(`Upload of ${Object.keys(data)} failed: ${error}.`);
205+
throw new Error(`Uploading ${Object.keys(data)} failed`);
206+
}
207+
}
208+
188209
async doAfterSubmit(addonId, newVersionId, editUrl) {
189210
if (this.approvalCheckTimeout > 0) {
190211
const fileUrl = new URL(
@@ -237,7 +258,7 @@ export default class Client {
237258
}
238259

239260
async fetch(url, method = 'GET', body) {
240-
log.info(`Fetching URL: ${url.href}`);
261+
log.info(`${method}ing URL: ${url.href}`);
241262
let headers = {
242263
Authorization: await this.apiAuth.getAuthHeader(),
243264
Accept: 'application/json',
@@ -350,13 +371,19 @@ export default class Client {
350371
uploadUuid,
351372
savedIdPath,
352373
metaDataJson,
374+
versionPatchData,
353375
saveIdToFileFunc = saveIdToFile,
354376
) {
355377
const {
356378
guid: addonId,
357379
version: { id: newVersionId, edit_url: editUrl },
358380
} = await this.doNewAddonSubmit(uploadUuid, metaDataJson);
359381

382+
if (versionPatchData) {
383+
log.info('Submitting source zip');
384+
await this.doFormDataPatch(versionPatchData, addonId, newVersionId);
385+
}
386+
360387
await saveIdToFileFunc(savedIdPath, addonId);
361388
log.info(`Generated extension ID: ${addonId}.`);
362389
log.info('You must add the following to your manifest:');
@@ -365,11 +392,15 @@ export default class Client {
365392
return this.doAfterSubmit(addonId, newVersionId, editUrl);
366393
}
367394

368-
async putVersion(uploadUuid, addonId, metaDataJson) {
395+
async putVersion(uploadUuid, addonId, metaDataJson, versionPatchData) {
369396
const {
370397
version: { id: newVersionId, edit_url: editUrl },
371398
} = await this.doNewAddonOrVersionSubmit(addonId, uploadUuid, metaDataJson);
372399

400+
if (versionPatchData) {
401+
log.info('Submitting source zip');
402+
await this.doFormDataPatch(versionPatchData, addonId, newVersionId);
403+
}
373404
return this.doAfterSubmit(addonId, newVersionId, editUrl);
374405
}
375406
}
@@ -388,6 +419,7 @@ export async function signAddon({
388419
savedIdPath,
389420
savedUploadUuidPath,
390421
metaDataJson = {},
422+
versionSource,
391423
userAgentString,
392424
SubmitClient = Client,
393425
ApiAuthClass = JwtApiAuth,
@@ -423,14 +455,23 @@ export async function signAddon({
423455
channel,
424456
savedUploadUuidPath,
425457
);
458+
// if we have a source file we need to upload we patch after the create
459+
const versionPatchData = versionSource
460+
? { source: client.fileFromSync(versionSource) }
461+
: undefined;
426462

427463
// We specifically need to know if `id` has not been passed as a parameter because
428464
// it's the indication that a new add-on should be created, rather than a new version.
429465
if (id === undefined) {
430-
return client.postNewAddon(uploadUuid, savedIdPath, metaDataJson);
466+
return client.postNewAddon(
467+
uploadUuid,
468+
savedIdPath,
469+
metaDataJson,
470+
versionPatchData,
471+
);
431472
}
432473

433-
return client.putVersion(uploadUuid, id, metaDataJson);
474+
return client.putVersion(uploadUuid, id, metaDataJson, versionPatchData);
434475
}
435476

436477
export async function saveIdToFile(filePath, id) {

tests/unit/test-cmd/test.sign.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,23 @@ describe('sign', () => {
384384
});
385385
}));
386386

387+
it('passes the versionSource parameter to submissionAPI signer', () =>
388+
withTempDir((tmpDir) => {
389+
const stubs = getStubs();
390+
const versionSource = 'path/to/source.zip';
391+
return sign(tmpDir, stubs, {
392+
extraArgs: {
393+
versionSource,
394+
useSubmissionApi: true,
395+
channel: 'unlisted',
396+
},
397+
}).then(() => {
398+
sinon.assert.called(stubs.signingOptions.submitAddon);
399+
sinon.assert.calledWithMatch(stubs.signingOptions.submitAddon, {
400+
versionSource,
401+
});
402+
});
403+
}));
387404
it('returns a signing result', () =>
388405
withTempDir((tmpDir) => {
389406
const stubs = getStubs();

tests/unit/test-util/test.submit-addon.js

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ describe('util.submit-addon', () => {
5050
let getPreviousUuidOrUploadXpiStub;
5151
let postNewAddonStub;
5252
let putVersionStub;
53+
let fileFromSyncStub;
5354
const uploadUuid = '{some-upload-uuid}';
55+
const fakeFileFromSync = new File([], 'foo.xpi');
5456

5557
beforeEach(() => {
5658
statStub = sinon
@@ -61,13 +63,17 @@ describe('util.submit-addon', () => {
6163
.resolves(uploadUuid);
6264
postNewAddonStub = sinon.stub(Client.prototype, 'postNewAddon');
6365
putVersionStub = sinon.stub(Client.prototype, 'putVersion');
66+
fileFromSyncStub = sinon
67+
.stub(Client.prototype, 'fileFromSync')
68+
.returns(fakeFileFromSync);
6469
});
6570

6671
afterEach(() => {
6772
statStub.restore();
6873
getPreviousUuidOrUploadXpiStub.restore();
6974
postNewAddonStub.restore();
7075
putVersionStub.restore();
76+
fileFromSyncStub.restore();
7177
});
7278

7379
const signAddonDefaults = {
@@ -122,6 +128,7 @@ describe('util.submit-addon', () => {
122128
downloadDir,
123129
userAgentString,
124130
});
131+
sinon.assert.notCalled(fileFromSyncStub);
125132
});
126133

127134
it('calls postNewAddon if `id` is undefined', async () => {
@@ -187,6 +194,42 @@ describe('util.submit-addon', () => {
187194
metaDataJson,
188195
);
189196
});
197+
198+
it('includes source data to be patched if versionSource defined for new addon', async () => {
199+
const versionSource = 'path/to/source/zip';
200+
await signAddon({
201+
...signAddonDefaults,
202+
versionSource,
203+
});
204+
205+
sinon.assert.calledWith(fileFromSyncStub, versionSource);
206+
sinon.assert.calledWith(
207+
postNewAddonStub,
208+
uploadUuid,
209+
signAddonDefaults.savedIdPath,
210+
{},
211+
{ source: fakeFileFromSync },
212+
);
213+
});
214+
215+
it('includes source data to be patched if versionSource defined for new version', async () => {
216+
const versionSource = 'path/to/source/zip';
217+
const id = '@thisID';
218+
await signAddon({
219+
...signAddonDefaults,
220+
versionSource,
221+
id,
222+
});
223+
224+
sinon.assert.calledWith(fileFromSyncStub, versionSource);
225+
sinon.assert.calledWith(
226+
putVersionStub,
227+
uploadUuid,
228+
id,
229+
{},
230+
{ source: fakeFileFromSync },
231+
);
232+
});
190233
});
191234

192235
describe('Client', () => {
@@ -738,6 +781,42 @@ describe('util.submit-addon', () => {
738781
});
739782
});
740783

784+
describe('doFormDataPatch', () => {
785+
const addonId = 'some-addon-id';
786+
const versionId = 123456;
787+
const dataField1 = 'someField';
788+
const dataField2 = 'otherField';
789+
const data = { dataField1: 'value', dataField2: 0 };
790+
const formData = new FormData();
791+
formData.append(dataField1, data[dataField1]);
792+
formData.append(dataField2, data[dataField2]);
793+
794+
it('creates the url from addon and version', async () => {
795+
const client = new Client(clientDefaults);
796+
const fetchStub = sinon
797+
.stub(client, 'fetch')
798+
.resolves(new Response('', { ok: true, status: 200 }));
799+
await client.doFormDataPatch(data, addonId, versionId);
800+
const patchUrl = new URL(
801+
`addon/${addonId}/versions/${versionId}/`,
802+
client.apiUrl,
803+
);
804+
805+
sinon.assert.calledWith(fetchStub, patchUrl, 'PATCH', formData);
806+
});
807+
808+
it('catches and throws for non ok responses', async () => {
809+
const client = new Client(clientDefaults);
810+
sinon.stub(client, 'fetch').resolves();
811+
const response = client.doFormDataPatch(data, addonId, versionId);
812+
813+
assert.isRejected(
814+
response,
815+
`Uploading ${dataField1}${dataField2} failed`,
816+
);
817+
});
818+
});
819+
741820
describe('waitForApproval', () => {
742821
it('aborts approval wait after timeout', async () => {
743822
const client = new Client({
@@ -902,7 +981,13 @@ describe('util.submit-addon', () => {
902981
[{ body: sampleAddonDetail, status: 200 }],
903982
);
904983
addApprovalMocks(versionId);
905-
await client.postNewAddon(uploadUuid, idFile, {}, saveIdStub);
984+
await client.postNewAddon(
985+
uploadUuid,
986+
idFile,
987+
{},
988+
undefined,
989+
saveIdStub,
990+
);
906991
sinon.assert.calledWith(saveIdStub, idFile, sampleAddonDetail.guid);
907992
});
908993

@@ -920,6 +1005,92 @@ describe('util.submit-addon', () => {
9201005
await client.putVersion(uploadUuid, `${addonId}`, {});
9211006
});
9221007

1008+
describe('doFormDataPatch called correctly', () => {
1009+
const versionPatchData = { source: 'somesource' };
1010+
const metaDataJson = { some: 'metadata' };
1011+
const newVersionId = 123456;
1012+
const editUrl = 'http://some/url';
1013+
const stubbedClient = new Client(clientDefaults);
1014+
1015+
const submitResponse = {
1016+
guid: addonId,
1017+
version: { id: newVersionId, edit_url: editUrl },
1018+
};
1019+
sinon
1020+
.stub(stubbedClient, 'doNewAddonOrVersionSubmit')
1021+
.resolves(submitResponse);
1022+
sinon.stub(stubbedClient, 'doNewAddonSubmit').resolves(submitResponse);
1023+
sinon.stub(stubbedClient, 'doAfterSubmit').resolves();
1024+
1025+
let doFormDataPatchStub;
1026+
1027+
before(() => {
1028+
doFormDataPatchStub = sinon
1029+
.stub(stubbedClient, 'doFormDataPatch')
1030+
.resolves();
1031+
});
1032+
1033+
afterEach(() => {
1034+
doFormDataPatchStub.reset();
1035+
});
1036+
1037+
it('calls doFormDataPatch if versionPatchData is defined for postNewAddon', async () => {
1038+
const saveIdToFileStub = sinon.stub().resolves();
1039+
const savedIdPath = 'some/saved/id/path';
1040+
await stubbedClient.postNewAddon(
1041+
uploadUuid,
1042+
savedIdPath,
1043+
metaDataJson,
1044+
versionPatchData,
1045+
saveIdToFileStub,
1046+
);
1047+
1048+
sinon.assert.calledWith(
1049+
doFormDataPatchStub,
1050+
versionPatchData,
1051+
addonId,
1052+
newVersionId,
1053+
);
1054+
});
1055+
1056+
it('calls doFormDataPatch if versionPatchData is defined for putVersion', async () => {
1057+
await stubbedClient.putVersion(
1058+
uploadUuid,
1059+
addonId,
1060+
metaDataJson,
1061+
versionPatchData,
1062+
);
1063+
1064+
sinon.assert.called(doFormDataPatchStub);
1065+
sinon.assert.calledWith(
1066+
doFormDataPatchStub,
1067+
versionPatchData,
1068+
addonId,
1069+
newVersionId,
1070+
);
1071+
});
1072+
1073+
it('does not call doFormDataPatch is versionPatchData is undefined for postNewAddon', async () => {
1074+
const saveIdToFileStub = sinon.stub().resolves();
1075+
const savedIdPath = 'some/saved/id/path';
1076+
await stubbedClient.postNewAddon(
1077+
uploadUuid,
1078+
savedIdPath,
1079+
metaDataJson,
1080+
undefined,
1081+
saveIdToFileStub,
1082+
);
1083+
1084+
sinon.assert.notCalled(doFormDataPatchStub);
1085+
});
1086+
1087+
it('does not call doFormDataPatch is versionPatchData is undefined for putVersion', async () => {
1088+
await stubbedClient.putVersion(uploadUuid, addonId, metaDataJson);
1089+
1090+
sinon.assert.notCalled(doFormDataPatchStub);
1091+
});
1092+
});
1093+
9231094
describe('doAfterSubmit', () => {
9241095
const downloadUrl = 'https://a.download/url';
9251096
let approvalStub;

0 commit comments

Comments
 (0)