Skip to content

Allow JSON metadata to be submitted to addon submission API #2489

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/cmd/sign.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type SignParams = {|
timeout: number,
verbose?: boolean,
channel?: string,
amoMetadata?: string,
|};

export type SignOptions = {
Expand All @@ -47,6 +48,7 @@ export type SignOptions = {
submitAddon?: typeof defaultSubmitAddonSigner,
preValidatedManifest?: ExtensionManifest,
shouldExitProgram?: boolean,
asyncFsReadFile?: typeof defaultAsyncFsReadFile,
};

export default function sign(
Expand All @@ -64,12 +66,14 @@ export default function sign(
timeout,
verbose,
channel,
amoMetadata,
}: SignParams,
{
build = defaultBuilder,
preValidatedManifest,
signAddon = defaultAddonSigner,
submitAddon = defaultSubmitAddonSigner,
asyncFsReadFile = defaultAsyncFsReadFile,
}: SignOptions = {}
): Promise<SignResult> {
return withTempDir(
Expand Down Expand Up @@ -142,6 +146,16 @@ export default function sign(
);
}

let metaDataJson;
if (amoMetadata) {
const metadataFileBuffer = await asyncFsReadFile(amoMetadata);
try {
metaDataJson = JSON.parse(metadataFileBuffer.toString());
} catch (err) {
throw new UsageError('Invalid JSON in listing metadata');
}
}

const signSubmitArgs = {
apiKey,
apiSecret,
Expand All @@ -155,8 +169,10 @@ export default function sign(
let result;
try {
if (useSubmissionApi) {
// $FlowIgnore: we verify 'channel' is set above
result = await submitAddon({...signSubmitArgs, amoBaseUrl, channel});
result = await submitAddon(
// $FlowIgnore: we verify 'channel' is set above
{...signSubmitArgs, amoBaseUrl, channel, metaDataJson}
);
} else {
const { success, id: newId, downloadedFiles } = await signAddon({
...signSubmitArgs,
Expand Down
7 changes: 7 additions & 0 deletions src/program.js
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,13 @@ Example: $0 --help run.
'channel': {
describe: 'The channel for which to sign the addon. Either ' +
'\'listed\' or \'unlisted\'',
},
'amo-metadata': {
describe: 'Path to a JSON file containing an object with metadata ' +
'to be passed to the API. ' +
'See https://addons-server.readthedocs.io' +
'/en/latest/topics/api/addons.html for details. ' +
'Only used with `use-submission-api`',
type: 'string',
},
})
Expand Down
14 changes: 10 additions & 4 deletions src/util/submit-addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ export default class Client {

async doNewAddonSubmit(uuid: string, metaDataJson: Object): Promise<any> {
const url = new URL('addon/', this.apiUrl);
const jsonData = { version: { upload: uuid }, ...metaDataJson };
const jsonData = {
...metaDataJson, version: { upload: uuid, ...metaDataJson.version },
};
return this.fetchJson(url, 'POST', JSON.stringify(jsonData));
}

Expand All @@ -189,7 +191,9 @@ export default class Client {
metaDataJson: Object,
): Promise<typeof Response> {
const url = new URL(`addon/${addonId}/`, this.apiUrl);
const jsonData = { version: { upload: uuid }, ...metaDataJson };
const jsonData = {
...metaDataJson, version: { upload: uuid, ...metaDataJson.version },
};
return this.fetch(url, 'PUT', JSON.stringify(jsonData));
}

Expand Down Expand Up @@ -328,6 +332,7 @@ type signAddonParams = {|
xpiPath: string,
downloadDir: string,
channel: string,
metaDataJson?: Object,
SubmitClient?: typeof Client,
ApiAuthClass?: typeof JwtApiAuth,
|}
Expand All @@ -341,6 +346,7 @@ export async function signAddon({
xpiPath,
downloadDir,
channel,
metaDataJson = {},
SubmitClient = Client,
ApiAuthClass = JwtApiAuth,
}: signAddonParams): Promise<SignResult> {
Expand Down Expand Up @@ -372,8 +378,8 @@ export async function signAddon({
// 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.
if (id === undefined) {
return client.postNewAddon(xpiPath, channel, {});
return client.postNewAddon(xpiPath, channel, metaDataJson);
}

return client.putVersion(xpiPath, channel, id, {});
return client.putVersion(xpiPath, channel, id, metaDataJson);
}
49 changes: 49 additions & 0 deletions tests/unit/test-cmd/test.sign.js
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,55 @@ describe('sign', () => {
}
));

it(
'parses listing metadata as JSON and passes through to submitAddon',
() => withTempDir(
async (tmpDir) => {
const stubs = getStubs();
const metaDataJson = {version: {license: 'MPL2.0'}};
const amoMetadata = 'path/to/metadata.json';
const asyncFsReadFileStub = sinon.spy(
() => Promise.resolve(new Buffer(JSON.stringify(metaDataJson)))
);

return sign(tmpDir, stubs, {
extraArgs: {
useSubmissionApi: true, channel: 'listed', amoMetadata,
},
extraOptions: {
asyncFsReadFile: asyncFsReadFileStub,
},
}).then(() => {
sinon.assert.called(stubs.submitAddon);
sinon.assert.calledWithMatch(stubs.submitAddon, {metaDataJson});
sinon.assert.calledWith(asyncFsReadFileStub, amoMetadata);
});
}
));

it('raises an error on invalid JSON', () => withTempDir(
async (tmpDir) => {
const stubs = getStubs();
const amoMetadata = 'path/to/metadata.json';
const asyncFsReadFileStub = sinon.spy(
() => Promise.resolve(new Buffer('{"broken":"json"'))
);

const signPromise = sign(tmpDir, stubs, {
extraArgs: { amoMetadata },
extraOptions: {
asyncFsReadFile: asyncFsReadFileStub,
},
});
await assert.isRejected(signPromise, UsageError);
await assert.isRejected(
signPromise,
/Invalid JSON in listing metadata/,
);
sinon.assert.calledWith(asyncFsReadFileStub, amoMetadata);
}
));

describe('saveIdToSourceDir', () => {

it('saves an extension ID to file', () => withTempDir(
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/test-util/test.submit-addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,21 @@ describe('util.submit-addon', () => {
`Invalid AMO API base URL: ${amoBaseUrl}`
);
});

it('passes through metadata json object if defined', async () => {
const metaDataJson = {version: {license: 'MPL2.0'}};
await signAddon({
...signAddonDefaults,
metaDataJson,
});
sinon.assert.notCalled(putVersionStub);
sinon.assert.calledWith(
postNewAddonStub,
signAddonDefaults.xpiPath,
signAddonDefaults.channel,
metaDataJson
);
});
});

describe('Client', () => {
Expand Down Expand Up @@ -352,6 +367,29 @@ describe('util.submit-addon', () => {
const returnData = await client.doNewAddonSubmit(uploadUuid, {});
expect(returnData).to.eql(sampleAddonDetail);
});

it('combines provided metaDataJson with upload uuid', async () => {
const client = new Client(clientDefaults);
const nodeFetchStub = sinon.stub(client, 'nodeFetch');
nodeFetchStub.callsFake(async () => {
return new JSONResponse(sampleAddonDetail, 202);
});
const uploadUuid = 'some-uuid';
const metaDataJson = {
version: {license: 'MPL2.0'}, categories: {firefox: ['other']},
};
const body = JSON.stringify({
version: {upload: uploadUuid, license: metaDataJson.version.license},
categories: metaDataJson.categories,
});

await client.doNewAddonSubmit(uploadUuid, metaDataJson);
sinon.assert.calledWith(
nodeFetchStub,
sinon.match.instanceOf(URL),
sinon.match({ method: 'POST', body })
);
});
});

describe('doNewAddonOrVersionSubmit', () => {
Expand All @@ -369,6 +407,30 @@ describe('util.submit-addon', () => {
const uploadUuid = 'some-uuid';
await client.doNewAddonOrVersionSubmit(guid, uploadUuid, {});
});

it('combines provided metaDataJson with upload uuid', async () => {
const client = new Client(clientDefaults);
const nodeFetchStub = sinon.stub(client, 'nodeFetch');
nodeFetchStub.callsFake(async () => {
return new JSONResponse(sampleAddonDetail, 202);
});
const uploadUuid = 'some-uuid';
const guid = '@some-addon-guid';
const metaDataJson = {
version: {license: 'MPL2.0'}, categories: {firefox: ['other']},
};
const body = JSON.stringify({
version: {upload: uploadUuid, license: metaDataJson.version.license},
categories: metaDataJson.categories,
});

await client.doNewAddonOrVersionSubmit(guid, uploadUuid, metaDataJson);
sinon.assert.calledWith(
nodeFetchStub,
sinon.match.instanceOf(URL),
sinon.match({ method: 'PUT', body })
);
});
});

describe('waitForApproval', () => {
Expand Down