Skip to content
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

Refactor pp card clone to use util instead of calling other command, Closes #5242 #5243

Closed
wants to merge 3 commits into from
Closed
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
64 changes: 51 additions & 13 deletions src/m365/pp/commands/card/card-clone.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { powerPlatform } from '../../../../utils/powerPlatform.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './card-clone.js';
import ppCardGetCommand from './card-get.js';

describe(commands.CARD_CLONE, () => {
let commandInfo: CommandInfo;
Expand All @@ -22,8 +21,55 @@ describe(commands.CARD_CLONE, () => {
const validName = 'CLI 365 Card';
const validNewName = 'new CLI 365 Card';
const envUrl = "https://contoso-dev.api.crm4.dynamics.com";
// const cardResponse = {
// "CardIdClone": "80cff342-ddf1-4633-aec1-6d3d131b29e0"
// };
const cardResponse = {
"CardIdClone": "80cff342-ddf1-4633-aec1-6d3d131b29e0"
solutionid: 'fd140aae-4df4-11dd-bd17-0019b9312238',
modifiedon: '2022-10-11T08:52:12Z',
'_owninguser_value': '7d48edd3-69fd-ec11-82e5-000d3ab87733',
overriddencreatedon: null,
ismanaged: false,
schemaversion: null,
tags: null,
importsequencenumber: null,
componentidunique: 'd7c1acb5-37a4-4873-b24e-34b18c15c6a5',
'_modifiedonbehalfby_value': null,
componentstate: 0,
statecode: 0,
name: validName,
versionnumber: 3044006,
utcconversiontimezonecode: null,
cardid: validId,
publishdate: null,
'_createdonbehalfby_value': null,
'_modifiedby_value': '7d48edd3-69fd-ec11-82e5-000d3ab87733',
createdon: '2022-10-11T08:52:12Z',
overwritetime: '1900-01-01T00:00:00Z',
'_owningbusinessunit_value': '2199f44c-195b-ec11-8f8f-000d3adca49c',
hiddentags: null,
description: ' ',
appdefinition: '{\'screens\':{\'main\':{\'template\':{\'type\':\'AdaptiveCard\',\'body\':[{\'type\':\'TextBlock\',\'size\':\'Medium\',\'weight\':\'bolder\',\'text\':\'Your card title goes here\'},{\'type\':\'TextBlock\',\'text\':\'Add and remove element to customize your new card.\',\'wrap\':true}],\'actions\':[],\'$schema\':\'http://adaptivecards.io/schemas/1.4.0/adaptive-card.json\',\'version\':\'1.4\'},\'verbs\':{\'submit\':\'echo\'}}},\'sampleData\':{\'main\':{}},\'connections\':{},\'variables\':{},\'flows\':{}}',
statuscode: 1,
remixsourceid: null,
sizes: null,
'_owningteam_value': null,
coowners: null,
'_createdby_value': '7d48edd3-69fd-ec11-82e5-000d3ab87733',
'_ownerid_value': '7d48edd3-69fd-ec11-82e5-000d3ab87733',
publishsourceid: null,
timezoneruleversionnumber: null,
iscustomizable: {
Value: true,
CanBeChanged: true,
ManagedPropertyLogicalName: 'iscustomizableanddeletable'
},
owninguser: {
azureactivedirectoryobjectid: '88e85b64-e687-4e0b-bbf4-f42f5f8e574c',
fullname: 'Contoso Admin',
systemuserid: '7d48edd3-69fd-ec11-82e5-000d3ab87733',
ownerid: '7d48edd3-69fd-ec11-82e5-000d3ab87733'
}
};

let log: string[];
Expand Down Expand Up @@ -60,8 +106,8 @@ describe(commands.CARD_CLONE, () => {
request.get,
request.post,
powerPlatform.getDynamicsInstanceApiUrl,
Cli.prompt,
Cli.executeCommandWithOutput
powerPlatform.getCardByName,
Cli.prompt
]);
});

Expand Down Expand Up @@ -102,15 +148,7 @@ describe(commands.CARD_CLONE, () => {
it('clones the specified card owned by the currently signed-in user based on the name', async () => {
sinon.stub(powerPlatform, 'getDynamicsInstanceApiUrl').callsFake(async () => envUrl);

sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command): Promise<any> => {
if (command === ppCardGetCommand) {
return ({
stdout: `{ "overwritetime": "1900-01-01T00:00:00Z", "_owningbusinessunit_value": "b419f090-fe22-ec11-b6e5-000d3ab596a1", "solutionid": "fd140aae-4df4-11dd-bd17-0019b9312238", "componentidunique": "e2b1d019-bd9a-491a-b888-693740711319", "_owninguser_value": "4f175d04-b952-ed11-bba2-000d3adf774e", "statecode": 0, "statuscode": 1, "ismanaged": false, "cardid": "${validId}", "_ownerid_value": "4f175d04-b952-ed11-bba2-000d3adf774e", "componentstate": 0, "modifiedon": "2022-10-29T08:22:46Z", "name": "${validName}", "_modifiedby_value": "4f175d04-b952-ed11-bba2-000d3adf774e", "versionnumber": 4463945, "createdon": "2022-10-29T08:22:46Z", "description": " ", "_createdby_value": "4f175d04-b952-ed11-bba2-000d3adf774e", "overriddencreatedon": null, "schemaversion": null, "importsequencenumber": null, "tags": null, "_modifiedonbehalfby_value": null, "utcconversiontimezonecode": null, "publishdate": null, "_createdonbehalfby_value": null, "hiddentags": null, "remixsourceid": null, "sizes": null, "coowners": null, "_owningteam_value": null, "publishsourceid": null, "timezoneruleversionnumber": null, "iscustomizable": { "Value": true, "CanBeChanged": true, "ManagedPropertyLogicalName": "iscustomizableanddeletable"}}`
});
}

throw new CommandError('Unknown case');
});
sinon.stub(powerPlatform, 'getCardByName').resolves(cardResponse);

sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `https://contoso-dev.api.crm4.dynamics.com/api/data/v9.1/CardCreateClone` &&
Expand Down
26 changes: 7 additions & 19 deletions src/m365/pp/commands/card/card-clone.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { Cli } from '../../../../cli/Cli.js';
import { Logger } from '../../../../cli/Logger.js';
import Command from '../../../../Command.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { powerPlatform } from '../../../../utils/powerPlatform.js';
import { validation } from '../../../../utils/validation.js';
import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js';
import commands from '../../commands.js';
import ppCardGetCommand, { Options as PpCardGetCommandOptions } from './card-get.js';

interface CommandArgs {
options: Options;
Expand Down Expand Up @@ -93,33 +90,24 @@ class PpCardCloneCommand extends PowerPlatformCommand {
await logger.logToStderr(`Cloning a card from '${args.options.id || args.options.name}'...`);
}

const res = await this.cloneCard(args);
await logger.log(res);
const res = await this.cloneCard(args, logger);
logger.log(res);
}

private async getCardId(args: CommandArgs): Promise<any> {
private async getCardId(args: CommandArgs, dynamicsApiUrl: string, logger: Logger): Promise<any> {
if (args.options.id) {
return args.options.id;
}

const options: PpCardGetCommandOptions = {
environmentName: args.options.environmentName,
name: args.options.name,
output: 'json',
debug: this.debug,
verbose: this.verbose
};

const output = await Cli.executeCommandWithOutput(ppCardGetCommand as Command, { options: { ...options, _: [] } });
const getCardOutput = JSON.parse(output.stdout);
return getCardOutput.cardid;
const card = await powerPlatform.getCardByName(dynamicsApiUrl, args.options.name!, logger, this.verbose);
return card.cardid;
}

private async cloneCard(args: CommandArgs): Promise<any> {
private async cloneCard(args: CommandArgs, logger: Logger): Promise<any> {
try {
const dynamicsApiUrl = await powerPlatform.getDynamicsInstanceApiUrl(args.options.environmentName, args.options.asAdmin);

const cardId = await this.getCardId(args);
const cardId = await this.getCardId(args, dynamicsApiUrl, logger);
const requestOptions: CliRequestOptions = {
url: `${dynamicsApiUrl}/api/data/v9.1/CardCreateClone`,
headers: {
Expand Down
134 changes: 134 additions & 0 deletions src/utils/powerPlatform.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,86 @@ import request from "../request.js";
import auth from '../Auth.js';
import { powerPlatform } from './powerPlatform.js';
import { sinonUtil } from "./sinonUtil.js";
import { Logger } from '../cli/Logger.js';

const validCardName = 'CLI 365 Card';
const envUrl = 'https://contoso-dev.api.crm4.dynamics.com';
const cardResponse = {
value: [
{
solutionid: 'fd140aae-4df4-11dd-bd17-0019b9312238',
modifiedon: '2022-10-11T08:52:12Z',
'_owninguser_value': '7d48edd3-69fd-ec11-82e5-000d3ab87733',
overriddencreatedon: null,
ismanaged: false,
schemaversion: null,
tags: null,
importsequencenumber: null,
componentidunique: 'd7c1acb5-37a4-4873-b24e-34b18c15c6a5',
'_modifiedonbehalfby_value': null,
componentstate: 0,
statecode: 0,
name: 'DummyCard',
versionnumber: 3044006,
utcconversiontimezonecode: null,
cardid: '69703efe-4149-ed11-bba2-000d3adf7537',
publishdate: null,
'_createdonbehalfby_value': null,
'_modifiedby_value': '7d48edd3-69fd-ec11-82e5-000d3ab87733',
createdon: '2022-10-11T08:52:12Z',
overwritetime: '1900-01-01T00:00:00Z',
'_owningbusinessunit_value': '2199f44c-195b-ec11-8f8f-000d3adca49c',
hiddentags: null,
description: ' ',
appdefinition: '{\'screens\':{\'main\':{\'template\':{\'type\':\'AdaptiveCard\',\'body\':[{\'type\':\'TextBlock\',\'size\':\'Medium\',\'weight\':\'bolder\',\'text\':\'Your card title goes here\'},{\'type\':\'TextBlock\',\'text\':\'Add and remove element to customize your new card.\',\'wrap\':true}],\'actions\':[],\'$schema\':\'http://adaptivecards.io/schemas/1.4.0/adaptive-card.json\',\'version\':\'1.4\'},\'verbs\':{\'submit\':\'echo\'}}},\'sampleData\':{\'main\':{}},\'connections\':{},\'variables\':{},\'flows\':{}}',
statuscode: 1,
remixsourceid: null,
sizes: null,
'_owningteam_value': null,
coowners: null,
'_createdby_value': '7d48edd3-69fd-ec11-82e5-000d3ab87733',
'_ownerid_value': '7d48edd3-69fd-ec11-82e5-000d3ab87733',
publishsourceid: null,
timezoneruleversionnumber: null,
iscustomizable: {
Value: true,
CanBeChanged: true,
ManagedPropertyLogicalName: 'iscustomizableanddeletable'
},
owninguser: {
azureactivedirectoryobjectid: '88e85b64-e687-4e0b-bbf4-f42f5f8e574c',
fullname: 'Contoso Admin',
systemuserid: '7d48edd3-69fd-ec11-82e5-000d3ab87733',
ownerid: '7d48edd3-69fd-ec11-82e5-000d3ab87733'
}
}
]
};

describe('utils/powerPlatform', () => {
let logger: Logger;
let log: string[];

before(() => {
sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve());
auth.service.connected = true;
});

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
});

afterEach(() => {
sinonUtil.restore([
request.get
Expand Down Expand Up @@ -77,4 +150,65 @@ describe('utils/powerPlatform', () => {
assert.deepStrictEqual(ex, Error(`The environment 'someRandomGuid' could not be retrieved. See the inner exception for more details: Random Error`));
}
});

it('throws error when multiple cards with same name were found', async () => {
const multipleCardsResponse = {
value: [
{ ["cardid"]: '69703efe-4149-ed11-bba2-000d3adf7537' },
{ ["cardid"]: '3a081d91-5ea8-40a7-8ac9-abbaa3fcb893' }
]
};
sinon.stub(request, 'get').callsFake(async (opts) => {
if ((opts.url === `https://contoso-dev.api.crm4.dynamics.com/api/data/v9.1/cards?$filter=name eq '${validCardName}'`)) {
if ((opts.headers?.accept as string)?.indexOf('application/json') === 0) {
return multipleCardsResponse;
}
}

throw 'Invalid request';
});

try {
await powerPlatform.getCardByName(envUrl, validCardName, logger, true);
assert.fail('No error message thrown.');
}
catch (ex) {
assert.deepStrictEqual(ex, Error(`Multiple cards with name 'CLI 365 Card' found: 69703efe-4149-ed11-bba2-000d3adf7537,3a081d91-5ea8-40a7-8ac9-abbaa3fcb893`));
}
});

it('throws error when no card found', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if ((opts.url === `https://contoso-dev.api.crm4.dynamics.com/api/data/v9.1/cards?$filter=name eq '${validCardName}'`)) {
if ((opts.headers?.accept as string)?.indexOf('application/json') === 0) {
return ({ "value": [] });
}
}

throw 'Invalid request';
});

try {
await powerPlatform.getCardByName(envUrl, validCardName);
assert.fail('No error message thrown.');
}
catch (ex) {
assert.deepStrictEqual(ex, Error(`The specified card 'CLI 365 Card' does not exist.`));
}
});

it('retrieves a specific card with the name parameter', async () => {
sinon.stub(request, 'get').callsFake(async opts => {
if ((opts.url === `https://contoso-dev.api.crm4.dynamics.com/api/data/v9.1/cards?$filter=name eq '${validCardName}'`)) {
if ((opts.headers?.accept as string)?.indexOf('application/json') === 0) {
return cardResponse;
}
}

throw `Invalid request ${opts.url}`;
});

const actual = await powerPlatform.getCardByName(envUrl, validCardName);
assert.strictEqual(actual, cardResponse.value[0]);
});
});
34 changes: 34 additions & 0 deletions src/utils/powerPlatform.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Logger } from "../cli/Logger.js";
import request, { CliRequestOptions } from "../request.js";
import { formatting } from "./formatting.js";

Expand Down Expand Up @@ -29,5 +30,38 @@ export const powerPlatform = {
catch (ex: any) {
throw Error(`The environment '${environment}' could not be retrieved. See the inner exception for more details: ${ex.message}`);
}
},

/**
* Get a card by name
* Returns the card
* @param dynamicsApiUrl The dynamics api url of the environment
* @param name The name of the app.
* @param logger The logger object
* @param verbose Set for verbose logging
*/
async getCardByName(dynamicsApiUrl: string, name: string, logger?: Logger, verbose?: boolean): Promise<any> {
if (verbose && logger) {
logger.logToStderr(`Retrieving the card with name ${name}`);
}
const requestOptions: CliRequestOptions = {
url: `${dynamicsApiUrl}/api/data/v9.1/cards?$filter=name eq '${name}'`,
headers: {
accept: 'application/json;odata.metadata=none'
},
responseType: 'json'
};

const result = await request.get<{ value: any[] }>(requestOptions);

if (result.value.length === 0) {
throw Error(`The specified card '${name}' does not exist.`);
}

if (result.value.length > 1) {
throw Error(`Multiple cards with name '${name}' found: ${result.value.map(x => x.cardid).join(',')}`);
}

return result.value[0];
}
};