diff --git a/src/m365/pp/commands/card/card-clone.spec.ts b/src/m365/pp/commands/card/card-clone.spec.ts index 4fc4364230c..e241e5d81a8 100644 --- a/src/m365/pp/commands/card/card-clone.spec.ts +++ b/src/m365/pp/commands/card/card-clone.spec.ts @@ -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; @@ -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[]; @@ -60,8 +106,8 @@ describe(commands.CARD_CLONE, () => { request.get, request.post, powerPlatform.getDynamicsInstanceApiUrl, - Cli.prompt, - Cli.executeCommandWithOutput + powerPlatform.getCardByName, + Cli.prompt ]); }); @@ -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 => { - 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` && diff --git a/src/m365/pp/commands/card/card-clone.ts b/src/m365/pp/commands/card/card-clone.ts index 53f5dcd2be7..d9da854d354 100644 --- a/src/m365/pp/commands/card/card-clone.ts +++ b/src/m365/pp/commands/card/card-clone.ts @@ -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; @@ -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 { + private async getCardId(args: CommandArgs, dynamicsApiUrl: string, logger: Logger): Promise { 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 { + private async cloneCard(args: CommandArgs, logger: Logger): Promise { 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: { diff --git a/src/utils/powerPlatform.spec.ts b/src/utils/powerPlatform.spec.ts index bad8edcdf10..2c6fc9d908a 100644 --- a/src/utils/powerPlatform.spec.ts +++ b/src/utils/powerPlatform.spec.ts @@ -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 @@ -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]); + }); }); \ No newline at end of file diff --git a/src/utils/powerPlatform.ts b/src/utils/powerPlatform.ts index 0922020ada4..4dd0f0436bb 100644 --- a/src/utils/powerPlatform.ts +++ b/src/utils/powerPlatform.ts @@ -1,3 +1,4 @@ +import { Logger } from "../cli/Logger.js"; import request, { CliRequestOptions } from "../request.js"; import { formatting } from "./formatting.js"; @@ -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 { + 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]; } }; \ No newline at end of file