From 4afda8623c188671f122fa8d74cbbe82dbcbdb8b Mon Sep 17 00:00:00 2001 From: Nico De Cleyre Date: Fri, 1 Nov 2024 13:23:54 +0100 Subject: [PATCH 1/4] First commit --- docs/docs/cmd/pp/website/website-get.mdx | 153 +++++++++++++ docs/src/config/sidebars.ts | 9 + src/m365/pp/commands.ts | 3 +- .../pp/commands/website/website-get.spec.ts | 212 ++++++++++++++++++ src/m365/pp/commands/website/website-get.ts | 77 +++++++ src/utils/powerPlatform.spec.ts | 204 ++++++++++++++++- src/utils/powerPlatform.ts | 78 +++++++ src/utils/validation.spec.ts | 12 + src/utils/validation.ts | 5 + 9 files changed, 751 insertions(+), 2 deletions(-) create mode 100644 docs/docs/cmd/pp/website/website-get.mdx create mode 100644 src/m365/pp/commands/website/website-get.spec.ts create mode 100644 src/m365/pp/commands/website/website-get.ts diff --git a/docs/docs/cmd/pp/website/website-get.mdx b/docs/docs/cmd/pp/website/website-get.mdx new file mode 100644 index 00000000000..7e460c59f1a --- /dev/null +++ b/docs/docs/cmd/pp/website/website-get.mdx @@ -0,0 +1,153 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# pp website get + +Gets information about the specified Power Pages websites. + +## Usage + +```sh +m365 pp website get [options] +``` + +## Options + +```md definition-list +`--url [url]` +: The URL of the website to retrieve. Specify either `url`, `name` or `id`. + +`-n, --name [name]` +: The name of the website to retrieve. Specify either `url`, `name` or `id`. + +`-i, --id [id]` +: The WebSiteId (GUID) of the website to retrieve. Specify either `url`, `name` or `id`. + +`-e, --environmentName ` +: The name of the environment for which to retrieve Power Pages website from. +``` + + + +## Examples + +Retrieve Demo Power Pages website by name in the given environment using `name`. + +```sh +m365 pp website get --name Demo --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5 +``` + +Retrieve Demo Power Pages website by name in the given environment using `id`. + +```sh +m365 pp website get --id 4916bb2c-91e1-4716-91d5-b6171928fac9 --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5 +``` + +Retrieve Demo Power Pages website by name in the given environment using `url`. + +```sh +m365 pp website get --url https://site-0uaq9.powerappsportals.com --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5 +``` + +## Response + + + + + ```json + { + "@odata.metadata": "https://api.powerplatform.com/powerpages/environments/Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0/websites/$metadata#Websites", + "id": "4916bb2c-91e1-4716-91d5-b6171928fac9", + "name": "Site 1", + "createdOn": "2024-10-27T12:00:03", + "templateName": "DefaultPortalTemplate", + "websiteUrl": "https://site-0uaq9.powerappsportals.com", + "tenantId": "727dc1e9-3cd1-4d1f-8102-ab5c936e52f0", + "dataverseInstanceUrl": "https://org0cd4b2b9.crm4.dynamics.com/", + "environmentName": "Contoso (default)", + "environmentId": "Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0", + "dataverseOrganizationId": "2d58aeac-74d4-4939-98d1-e05a70a655ba", + "selectedBaseLanguage": 1033, + "customHostNames": [], + "websiteRecordId": "5eb107a6-5ac2-4e1c-a3b9-d5c21bbc10ce", + "subdomain": "site-0uaq9", + "packageInstallStatus": "Installed", + "type": "Trial", + "trialExpiringInDays": 86, + "suspendedWebsiteDeletingInDays": 93, + "packageVersion": "9.6.9.39", + "isEarlyUpgradeEnabled": false, + "isCustomErrorEnabled": true, + "applicationUserAadAppId": "3f57aca7-5051-41b2-989d-26da8af7a53e", + "ownerId": "33469a62-c3af-4cfe-b893-854eceab96da", + "status": "OperationComplete", + "siteVisibility": "private", + "dataModel": "Enhanced" + } + ``` + + + + + ```text + id : 4916bb2c-91e1-4716-91d5-b6171928fac9 + name : Site 1 + siteVisibility: private + status : OperationComplete + subdomain : site-0uaq9 + tenantId : 727dc1e9-3cd1-4d1f-8102-ab5c936e52f0 + type : Trial + websiteUrl : https://site-0uaq9.powerappsportals.com + ``` + + + + + ```csv + @odata.metadata,id,name,createdOn,templateName,websiteUrl,tenantId,dataverseInstanceUrl,environmentName,environmentId,dataverseOrganizationId,selectedBaseLanguage,websiteRecordId,subdomain,packageInstallStatus,type,trialExpiringInDays,suspendedWebsiteDeletingInDays,packageVersion,isEarlyUpgradeEnabled,isCustomErrorEnabled,applicationUserAadAppId,ownerId,status,siteVisibility,dataModel + https://api.powerplatform.com/powerpages/environments/Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0/websites/$metadata#Websites,4916bb2c-91e1-4716-91d5-b6171928fac9,Site 1,2024-10-27T12:00:03,DefaultPortalTemplate,https://site-0uaq9.powerappsportals.com,727dc1e9-3cd1-4d1f-8102-ab5c936e52f0,https://org0cd4b2b9.crm4.dynamics.com/,Contoso (default),Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0,2d58aeac-74d4-4939-98d1-e05a70a655ba,1033,5eb107a6-5ac2-4e1c-a3b9-d5c21bbc10ce,site-0uaq9,Installed,Trial,86,93,9.6.9.39,0,1,3f57aca7-5051-41b2-989d-26da8af7a53e,33469a62-c3af-4cfe-b893-854eceab96da,OperationComplete,private,Enhanced + ``` + + + + + ```md + # pp website get --debug "false" --verbose "false" --name "Site 1" --environmentName "Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0" + + Date: 27/10/2024 + + ## Site 1 (4916bb2c-91e1-4716-91d5-b6171928fac9) + + Property | Value + ---------|------- + @odata.metadata | https://api.powerplatform.com/powerpages/environments/Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0/websites/$metadata#Websites + id | 4916bb2c-91e1-4716-91d5-b6171928fac9 + name | Site 1 + createdOn | 2024-10-27T12:00:03 + templateName | DefaultPortalTemplate + websiteUrl | https://site-0uaq9.powerappsportals.com + tenantId | 727dc1e9-3cd1-4d1f-8102-ab5c936e52f0 + dataverseInstanceUrl | https://org0cd4b2b9.crm4.dynamics.com/ + environmentName | Contoso (default) + environmentId | Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0 + dataverseOrganizationId | 2d58aeac-74d4-4939-98d1-e05a70a655ba + selectedBaseLanguage | 1033 + websiteRecordId | 5eb107a6-5ac2-4e1c-a3b9-d5c21bbc10ce + subdomain | site-0uaq9 + packageInstallStatus | Installed + type | Trial + trialExpiringInDays | 86 + suspendedWebsiteDeletingInDays | 93 + packageVersion | 9.6.9.39 + isEarlyUpgradeEnabled | false + isCustomErrorEnabled | true + applicationUserAadAppId | 3f57aca7-5051-41b2-989d-26da8af7a53e + ownerId | 33469a62-c3af-4cfe-b893-854eceab96da + status | OperationComplete + siteVisibility | private + dataModel | Enhanced + ``` + + + diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index c995ffe5bdb..c428bbfad9e 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -1799,6 +1799,15 @@ const sidebars: SidebarsConfig = { id: 'cmd/pp/tenant/tenant-settings-set' } ] + }, + { + website: [ + { + type: 'doc', + label: 'website get', + id: 'cmd/pp/website/website-get' + } + ] } ] }, diff --git a/src/m365/pp/commands.ts b/src/m365/pp/commands.ts index dae86ad2616..d11b10eb68d 100644 --- a/src/m365/pp/commands.ts +++ b/src/m365/pp/commands.ts @@ -31,5 +31,6 @@ export default { SOLUTION_PUBLISHER_LIST: `${prefix} solution publisher list`, SOLUTION_PUBLISHER_REMOVE: `${prefix} solution publisher remove`, TENANT_SETTINGS_LIST: `${prefix} tenant settings list`, - TENANT_SETTINGS_SET: `${prefix} tenant settings set` + TENANT_SETTINGS_SET: `${prefix} tenant settings set`, + WEBSITE_GET: `${prefix} website get` }; \ No newline at end of file diff --git a/src/m365/pp/commands/website/website-get.spec.ts b/src/m365/pp/commands/website/website-get.spec.ts new file mode 100644 index 00000000000..cec89aa6b2d --- /dev/null +++ b/src/m365/pp/commands/website/website-get.spec.ts @@ -0,0 +1,212 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import { z } from 'zod'; +import auth from '../../../../Auth.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { CommandError } from '../../../../Command.js'; +import { telemetry } from '../../../../telemetry.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import commands from '../../commands.js'; +import command from './website-get.js'; +import { powerPlatform } from '../../../../utils/powerPlatform.js'; +import { accessToken } from '../../../../utils/accessToken.js'; + +const environment = 'Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0'; +const powerPageResponse = { + "@odata.metadata": "https://api.powerplatform.com/powerpages/environments/Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0/websites/$metadata#Websites", + "id": "4916bb2c-91e1-4716-91d5-b6171928fac9", + "name": "Site 1", + "createdOn": "2024-10-27T12:00:03", + "templateName": "DefaultPortalTemplate", + "websiteUrl": "https://site-0uaq9.powerappsportals.com", + "tenantId": "727dc1e9-3cd1-4d1f-8102-ab5c936e52f0", + "dataverseInstanceUrl": "https://org0cd4b2b9.crm4.dynamics.com/", + "environmentName": "Contoso (default)", + "environmentId": "Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0", + "dataverseOrganizationId": "2d58aeac-74d4-4939-98d1-e05a70a655ba", + "selectedBaseLanguage": 1033, + "customHostNames": [], + "websiteRecordId": "5eb107a6-5ac2-4e1c-a3b9-d5c21bbc10ce", + "subdomain": "site-0uaq9", + "packageInstallStatus": "Installed", + "type": "Trial", + "trialExpiringInDays": 86, + "suspendedWebsiteDeletingInDays": 93, + "packageVersion": "9.6.9.39", + "isEarlyUpgradeEnabled": false, + "isCustomErrorEnabled": true, + "applicationUserAadAppId": "3f57aca7-5051-41b2-989d-26da8af7a53e", + "ownerId": "33469a62-c3af-4cfe-b893-854eceab96da", + "status": "OperationComplete", + "siteVisibility": "private", + "dataModel": "Enhanced" +}; + +describe(commands.WEBSITE_GET, () => { + let log: any[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertDelegatedAccessToken').returns(); + auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + powerPlatform.getWebsiteById, + powerPlatform.getWebsiteByName, + powerPlatform.getWebsiteByUrl + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name.startsWith(commands.WEBSITE_GET), true); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('defines correct properties for the default output', () => { + assert.deepStrictEqual(command.defaultProperties(), ['id', 'name', 'websiteUrl', 'tenantId', 'subdomain', 'type', 'status', 'siteVisibility']); + }); + + it('retrieves the information for the Power Page website by url', async () => { + sinon.stub(powerPlatform, 'getWebsiteByUrl').resolves(powerPageResponse); + + await command.action(logger, { options: { environmentName: environment, url: 'https://site-0uaq9.powerappsportals.com' } }); + assert(loggerLogSpy.calledWith(powerPageResponse)); + }); + + it('retrieves the information for the Power Page website by name', async () => { + sinon.stub(powerPlatform, 'getWebsiteByName').resolves(powerPageResponse); + + await command.action(logger, { options: { environmentName: environment, name: 'Site 1' } }); + assert(loggerLogSpy.calledWith(powerPageResponse)); + }); + + it('retrieves the information for the Power Page website by id', async () => { + sinon.stub(powerPlatform, 'getWebsiteById').resolves(powerPageResponse); + + await command.action(logger, { options: { environmentName: environment, id: '4916bb2c-91e1-4716-91d5-b6171928fac9' } }); + assert(loggerLogSpy.calledWith(powerPageResponse)); + }); + + it('correctly handles error when getting information for a site that doesn\'t exist', async () => { + sinon.stub(powerPlatform, 'getWebsiteByName').callsFake(() => { throw new Error('The specified Power Page website \'Site 1\' does not exist.'); }); + + await assert.rejects(command.action(logger, { options: { verbose: true, environmentName: environment, name: 'Site 1' } } as any), new CommandError('The specified Power Page website \'Site 1\' does not exist.')); + }); + + it('fails validation if the url option is not a valid SharePoint site URL', async () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environment, url: 'https://site-0uaq9.contoso.com' }); + assert.notStrictEqual(actual, true); + }); + + it('passes validation if the url option is a valid SharePoint site URL', async () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environment, url: 'https://site-0uaq9.powerappsportals.com' }); + assert.strictEqual(actual.success, true); + }); + + it('fails validation if url and name are used at the same time', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: environment, + url: 'https://site-0uaq9.powerappsportals.com', + name: 'Site 1' + }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation if url and id are used at the same time', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: environment, + url: 'https://site-0uaq9.powerappsportals.com', + id: '4916bb2c-91e1-4716-91d5-b6171928fac9' + }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation if name and id are used at the same time', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: environment, + id: '4916bb2c-91e1-4716-91d5-b6171928fac9', + name: 'Site 1' + }); + assert.strictEqual(actual.success, false); + }); + + it('passes validation with only url', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: environment, + url: 'https://site-0uaq9.powerappsportals.com' + }); + assert.strictEqual(actual.success, true); + }); + + it('passes validation with only id', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: environment, + id: '4916bb2c-91e1-4716-91d5-b6171928fac9' + }); + assert.strictEqual(actual.success, true); + }); + + it('passes validation with only name', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: environment, + name: 'Site 1' + }); + assert.strictEqual(actual.success, true); + }); + + it('fails validation if url, id, and name are all used at the same time', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: environment, + url: 'https://site-0uaq9.powerappsportals.com', + id: '4916bb2c-91e1-4716-91d5-b6171928fac9', + name: 'Site 1' + }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation if neither url, id, nor name are provided', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: environment + }); + assert.strictEqual(actual.success, false); + }); +}); diff --git a/src/m365/pp/commands/website/website-get.ts b/src/m365/pp/commands/website/website-get.ts new file mode 100644 index 00000000000..97d1303fbb4 --- /dev/null +++ b/src/m365/pp/commands/website/website-get.ts @@ -0,0 +1,77 @@ +import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; +import { powerPlatform } from '../../../../utils/powerPlatform.js'; +import { validation } from '../../../../utils/validation.js'; +import { zod } from '../../../../utils/zod.js'; +import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; +import commands from '../../commands.js'; +import { z } from 'zod'; + +const options = globalOptionsZod + .extend({ + url: zod.alias('u', z.string().optional() + .refine(url => url === undefined || validation.isValidPowerPagesUrl(url) === true, url => ({ + message: `'${url}' is not a valid Power Pages URL.` + })) + ), + id: zod.alias('i', z.string().uuid().optional()), + name: zod.alias('n', z.string().optional()), + environmentName: zod.alias('e', z.string()), + asAdmin: z.boolean().default(false) + }).strict(); +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} + +class PpWebsiteListCommand extends PowerPlatformCommand { + public get name(): string { + return commands.WEBSITE_GET; + } + + public get description(): string { + return 'Gets information about the specified Power Pages website.'; + } + + public defaultProperties(): string[] | undefined { + return ['id', 'name', 'websiteUrl', 'tenantId', 'subdomain', 'type', 'status', 'siteVisibility']; + } + + public get schema(): z.ZodTypeAny | undefined { + return options; + } + + public getRefinedSchema(schema: typeof options): z.ZodEffects | undefined { + return schema + .refine(options => (options.url !== undefined || options.id !== undefined || options.name !== undefined) && !(options.url !== undefined && options.id !== undefined) && !(options.url !== undefined && options.name !== undefined) && !(options.id !== undefined && options.name !== undefined), { + message: `Either url, id or name is required, but not multiple.` + }); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + if (this.verbose) { + await logger.logToStderr(`Retrieving the website...`); + } + + try { + let item = null; + + if (args.options.id) { + item = await powerPlatform.getWebsiteById(args.options.environmentName, args.options.id); + } + else if (args.options.name) { + item = await powerPlatform.getWebsiteByName(args.options.environmentName, args.options.name); + } + else if (args.options.url) { + item = await powerPlatform.getWebsiteByUrl(args.options.environmentName, args.options.url); + } + await logger.log(item); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new PpWebsiteListCommand(); \ No newline at end of file diff --git a/src/utils/powerPlatform.spec.ts b/src/utils/powerPlatform.spec.ts index a7346c908fa..c47efb9870a 100644 --- a/src/utils/powerPlatform.spec.ts +++ b/src/utils/powerPlatform.spec.ts @@ -4,16 +4,95 @@ import request from "../request.js"; import auth from '../Auth.js'; import { powerPlatform } from './powerPlatform.js'; import { sinonUtil } from "./sinonUtil.js"; +import { cli } from '../cli/cli.js'; +import { settingsNames } from '../settingsNames.js'; + +const environment = 'Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0'; +const powerPageResponse = { + value: [{ + "@odata.metadata": "https://api.powerplatform.com/powerpages/environments/Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0/websites/$metadata#Websites", + "id": "4916bb2c-91e1-4716-91d5-b6171928fac9", + "name": "Site 1", + "createdOn": "2024-10-27T12:00:03", + "templateName": "DefaultPortalTemplate", + "websiteUrl": "https://site-0uaq9.powerappsportals.com", + "tenantId": "727dc1e9-3cd1-4d1f-8102-ab5c936e52f0", + "dataverseInstanceUrl": "https://org0cd4b2b9.crm4.dynamics.com/", + "environmentName": "Contoso (default)", + "environmentId": "Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0", + "dataverseOrganizationId": "2d58aeac-74d4-4939-98d1-e05a70a655ba", + "selectedBaseLanguage": 1033, + "customHostNames": [], + "websiteRecordId": "5eb107a6-5ac2-4e1c-a3b9-d5c21bbc10ce", + "subdomain": "site-0uaq9", + "packageInstallStatus": "Installed", + "type": "Trial", + "trialExpiringInDays": 86, + "suspendedWebsiteDeletingInDays": 93, + "packageVersion": "9.6.9.39", + "isEarlyUpgradeEnabled": false, + "isCustomErrorEnabled": true, + "applicationUserAadAppId": "3f57aca7-5051-41b2-989d-26da8af7a53e", + "ownerId": "33469a62-c3af-4cfe-b893-854eceab96da", + "status": "OperationComplete", + "siteVisibility": "private", + "dataModel": "Enhanced" + }, + { + "@odata.metadata": "https://api.powerplatform.com/powerpages/environments/Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0/websites/$metadata#Websites", + "id": "dc2b0aa4-4449-4667-b1a8-41017b8f874c", + "name": "Site 2", + "createdOn": "2024-10-27T12:02:59", + "templateName": "DefaultPortalTemplate", + "websiteUrl": "https://site-aa9wk.powerappsportals.com", + "tenantId": "727dc1e9-3cd1-4d1f-8102-ab5c936e52f0", + "dataverseInstanceUrl": "https://org0cd4b2b9.crm4.dynamics.com/", + "environmentName": "Contoso (default)", + "environmentId": "Default-727dc1e9-3cd1-4d1f-8102-ab5c936e52f0", + "dataverseOrganizationId": "2d58aeac-74d4-4939-98d1-e05a70a655ba", + "selectedBaseLanguage": 1033, + "customHostNames": [], + "websiteRecordId": "bc59fb78-d685-4b70-b9e3-531ece45536d", + "subdomain": "site-aa9wk", + "packageInstallStatus": "Installed", + "type": "Trial", + "trialExpiringInDays": 86, + "suspendedWebsiteDeletingInDays": 93, + "packageVersion": "9.6.9.39", + "isEarlyUpgradeEnabled": false, + "isCustomErrorEnabled": true, + "applicationUserAadAppId": "3f57aca7-5051-41b2-989d-26da8af7a53e", + "ownerId": "33469a62-c3af-4cfe-b893-854eceab96da", + "status": "OperationComplete", + "siteVisibility": "private", + "dataModel": "Enhanced" + }] +}; + +const MultiplePowerPageResponseWithSameName = { + ...powerPageResponse, + value: [ + ...powerPageResponse.value.slice(0, 1), + { + ...powerPageResponse.value[1], + name: "Site 1" + } + ] +}; describe('utils/powerPlatform', () => { before(() => { sinon.stub(auth, 'restoreAuth').resolves(); auth.connection.active = true; + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => settingName === settingsNames.prompt ? false : defaultValue); }); afterEach(() => { sinonUtil.restore([ - request.get + request.get, + powerPlatform.getWebsiteById, + powerPlatform.getWebsiteByName, + powerPlatform.getWebsiteByUrl ]); }); @@ -77,4 +156,127 @@ describe('utils/powerPlatform', () => { assert.deepStrictEqual(ex, Error(`The environment 'someRandomGuid' could not be retrieved. See the inner exception for more details: Random Error`)); } }); + + //#region Power Page websites + it('returns correct Power Page website by id', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if ((opts.url === `https://api.powerplatform.com/powerpages/environments/${environment}/websites/4916bb2c-91e1-4716-91d5-b6171928fac9?api-version=2022-03-01-preview`)) { + if (opts.headers && + opts.headers.accept && + (opts.headers.accept as string).indexOf('application/json') === 0) { + return powerPageResponse.value[0]; + } + } + + throw 'Invalid request'; + }); + + const actual = await powerPlatform.getWebsiteById(environment, '4916bb2c-91e1-4716-91d5-b6171928fac9'); + assert.strictEqual(actual, powerPageResponse.value[0]); + }); + + it('handles error when using id', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if ((opts.url === `https://api.powerplatform.com/powerpages/environments/${environment}/websites/be13f9af-f73d-48d6-99c0-7097c03282fc?api-version=2022-03-01-preview`)) { + throw Error('Random Error'); + } + + return 'Invalid request'; + }); + + try { + await powerPlatform.getWebsiteById(environment, 'be13f9af-f73d-48d6-99c0-7097c03282fc'); + assert.fail('No error message thrown.'); + } + catch (ex) { + assert.deepStrictEqual(ex, Error(`The specified Power Page website with id 'be13f9af-f73d-48d6-99c0-7097c03282fc' does not exist.`)); + } + }); + + it('returns correct Power Page website by name', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if ((opts.url === `https://api.powerplatform.com/powerpages/environments/${environment}/websites?api-version=2022-03-01-preview`)) { + if (opts.headers && + opts.headers.accept && + (opts.headers.accept as string).indexOf('application/json') === 0) { + return powerPageResponse; + } + } + + throw 'Invalid request'; + }); + + const actual = await powerPlatform.getWebsiteByName(environment, 'Site 1'); + assert.strictEqual(actual, powerPageResponse.value[0]); + }); + + it('throws error message when multiple Power Page websites were found using name', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://api.powerplatform.com/powerpages/environments/${environment}/websites?api-version=2022-03-01-preview`) { + return MultiplePowerPageResponseWithSameName; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(powerPlatform.getWebsiteByName(environment, 'Site 1'), + new Error(`Multiple Power Page websites with name 'Site 1' found Found: https://site-0uaq9.powerappsportals.com, https://site-aa9wk.powerappsportals.com.`)); + }); + + it('handles selecting single result when multiple Power Page websites with the specified name found using name and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://api.powerplatform.com/powerpages/environments/${environment}/websites?api-version=2022-03-01-preview`) { + return MultiplePowerPageResponseWithSameName; + } + + throw 'Invalid Request'; + }); + + sinon.stub(cli, 'handleMultipleResultsFound').resolves(MultiplePowerPageResponseWithSameName.value[0]); + + const actual = await powerPlatform.getWebsiteByName(environment, 'Site 1'); + assert.deepStrictEqual(actual, MultiplePowerPageResponseWithSameName.value[0]); + }); + + it('handles no Power Page website found when using name', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if ((opts.url === `https://api.powerplatform.com/powerpages/environments/${environment}/websites?api-version=2022-03-01-preview`)) { + return { value: [] }; + } + + return 'Invalid request'; + }); + + await assert.rejects(powerPlatform.getWebsiteByName(environment, 'Site 1'), Error(`The specified Power Page website 'Site 1' does not exist.`)); + }); + + it('returns correct Power Page website by url', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if ((opts.url === `https://api.powerplatform.com/powerpages/environments/${environment}/websites?api-version=2022-03-01-preview`)) { + if (opts.headers && + opts.headers.accept && + (opts.headers.accept as string).indexOf('application/json') === 0) { + return powerPageResponse; + } + } + + throw 'Invalid request'; + }); + + const actual = await powerPlatform.getWebsiteByUrl(environment, 'https://site-0uaq9.powerappsportals.com'); + assert.strictEqual(actual, powerPageResponse.value[0]); + }); + + it('handles no Power Page website found when using url', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if ((opts.url === `https://api.powerplatform.com/powerpages/environments/${environment}/websites?api-version=2022-03-01-preview`)) { + return { value: [] }; + } + + return 'Invalid request'; + }); + + await assert.rejects(powerPlatform.getWebsiteByUrl(environment, 'https://site-0uaq9.powerappsportals.com'), Error(`The specified Power Page website with url 'https://site-0uaq9.powerappsportals.com' does not exist.`)); + }); + // #endregion }); \ No newline at end of file diff --git a/src/utils/powerPlatform.ts b/src/utils/powerPlatform.ts index 93cfdf4d352..de9909da187 100644 --- a/src/utils/powerPlatform.ts +++ b/src/utils/powerPlatform.ts @@ -1,8 +1,39 @@ +import { cli } from "../cli/cli.js"; import request, { CliRequestOptions } from "../request.js"; import { formatting } from "./formatting.js"; +import { odata } from "./odata.js"; const powerPlatformResource = 'https://api.bap.microsoft.com'; +export interface PowerPageWebsite { + id: string, + name: string, + createdOn: string, + templateName: string, + websiteUrl: string, + tenantId: string, + dataverseInstanceUrl: string, + environmentName: string, + environmentId: string, + dataverseOrganizationId: string, + selectedBaseLanguage: number, + customHostNames: Array, + websiteRecordId: string, + subdomain: string, + packageInstallStatus: string, + type: string, + trialExpiringInDays: number, + suspendedWebsiteDeletingInDays: number, + packageVersion: string, + isEarlyUpgradeEnabled: boolean, + isCustomErrorEnabled: boolean, + applicationUserAadAppId: string, + ownerId: string, + status: string, + siteVisibility: string, + dataModel: string +} + export const powerPlatform = { async getDynamicsInstanceApiUrl(environment: string, asAdmin?: boolean): Promise { let url: string = ''; @@ -28,5 +59,52 @@ export const powerPlatform = { catch (ex: any) { throw Error(`The environment '${environment}' could not be retrieved. See the inner exception for more details: ${ex.message}`); } + }, + + async getWebsiteById(environment: string, id: string): Promise { + const requestOptions: CliRequestOptions = { + url: `https://api.powerplatform.com/powerpages/environments/${environment}/websites/${id}?api-version=2022-03-01-preview`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + try { + const response = await request.get(requestOptions); + return response; + } + catch (ex: any) { + throw Error(`The specified Power Page website with id '${id}' does not exist.`); + } + }, + + async getWebsiteByName(environment: string, websiteName: string): Promise { + const response = await odata.getAllItems(`https://api.powerplatform.com/powerpages/environments/${environment}/websites?api-version=2022-03-01-preview`); + + const items = response.filter(response => response.name === websiteName); + + if (items.length === 0) { + throw Error(`The specified Power Page website '${websiteName}' does not exist.`); + } + + if (items.length > 1) { + const resultAsKeyValuePair = formatting.convertArrayToHashTable('websiteUrl', items); + return cli.handleMultipleResultsFound(`Multiple Power Page websites with name '${websiteName}' found`, resultAsKeyValuePair); + } + + return items[0]; + }, + + async getWebsiteByUrl(environment: string, url: string): Promise { + const response = await odata.getAllItems(`https://api.powerplatform.com/powerpages/environments/${environment}/websites?api-version=2022-03-01-preview`); + + const items = response.filter(response => response.websiteUrl === url); + + if (items.length === 0) { + throw Error(`The specified Power Page website with url '${url}' does not exist.`); + } + + return items[0]; } }; \ No newline at end of file diff --git a/src/utils/validation.spec.ts b/src/utils/validation.spec.ts index f295f588d84..f8f8f748142 100644 --- a/src/utils/validation.spec.ts +++ b/src/utils/validation.spec.ts @@ -685,4 +685,16 @@ describe('validation/validation', () => { const expected = true; assert.notStrictEqual(actual, expected); }); + + it('isValidPowerPagesUrl returns false if url is not a valid Power Pages website', () => { + const actual = validation.isValidPowerPagesUrl("https://site-0uaq9.contoso.com"); + const expected = true; + assert.notStrictEqual(actual, expected); + }); + + it('isValidPowerPagesUrl returns true if url is a valid Power Pages website', () => { + const actual = validation.isValidPowerPagesUrl("https://site-0uaq9.powerappsportals.com"); + const expected = true; + assert.strictEqual(actual, expected); + }); }); \ No newline at end of file diff --git a/src/utils/validation.ts b/src/utils/validation.ts index c2970fc7938..8a84fc80dff 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -417,5 +417,10 @@ export const validation = { .split(' ') .filter(permission => permission.indexOf('/') < 0); return invalidPermissions.length > 0 ? invalidPermissions : true; + }, + + isValidPowerPagesUrl(url: string): boolean { + const powerPagesUrlPattern = /^https:\/\/[a-zA-Z0-9-]+\.powerappsportals\.com$/; + return powerPagesUrlPattern.test(url); } }; \ No newline at end of file From 020c8b16c63f72fe3c030002bb591bd11b068969 Mon Sep 17 00:00:00 2001 From: Nico De Cleyre Date: Tue, 12 Nov 2024 19:37:33 +0100 Subject: [PATCH 2/4] Wrong command name fix --- src/m365/pp/commands/website/website-get.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/m365/pp/commands/website/website-get.ts b/src/m365/pp/commands/website/website-get.ts index 97d1303fbb4..c3fc70d7ee7 100644 --- a/src/m365/pp/commands/website/website-get.ts +++ b/src/m365/pp/commands/website/website-get.ts @@ -25,7 +25,7 @@ interface CommandArgs { options: Options; } -class PpWebsiteListCommand extends PowerPlatformCommand { +class PpWebsiteGetCommand extends PowerPlatformCommand { public get name(): string { return commands.WEBSITE_GET; } @@ -74,4 +74,4 @@ class PpWebsiteListCommand extends PowerPlatformCommand { } } -export default new PpWebsiteListCommand(); \ No newline at end of file +export default new PpWebsiteGetCommand(); \ No newline at end of file From f3e8b08ce7a9802ebc87a40b79d43980a377b73f Mon Sep 17 00:00:00 2001 From: Nico De Cleyre Date: Tue, 12 Nov 2024 20:00:05 +0100 Subject: [PATCH 3/4] Capital S.... Dull --- src/m365/pp/commands/website/website-get.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/m365/pp/commands/website/website-get.ts b/src/m365/pp/commands/website/website-get.ts index c3fc70d7ee7..746de80d69d 100644 --- a/src/m365/pp/commands/website/website-get.ts +++ b/src/m365/pp/commands/website/website-get.ts @@ -25,7 +25,7 @@ interface CommandArgs { options: Options; } -class PpWebsiteGetCommand extends PowerPlatformCommand { +class PpWebSiteGetCommand extends PowerPlatformCommand { public get name(): string { return commands.WEBSITE_GET; } @@ -74,4 +74,4 @@ class PpWebsiteGetCommand extends PowerPlatformCommand { } } -export default new PpWebsiteGetCommand(); \ No newline at end of file +export default new PpWebSiteGetCommand(); \ No newline at end of file From 9cd255bfbcc8690db569a38880698ea0160c5a41 Mon Sep 17 00:00:00 2001 From: Nico De Cleyre Date: Wed, 25 Dec 2024 12:24:50 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=A5=20=20Fix=20documentation=20and?= =?UTF-8?q?=20refine=20command=20options=20for=20website=20retrieval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs/cmd/pp/website/website-get.mdx | 8 ++++---- src/m365/pp/commands/website/website-get.ts | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/docs/cmd/pp/website/website-get.mdx b/docs/docs/cmd/pp/website/website-get.mdx index 7e460c59f1a..70e360dedd4 100644 --- a/docs/docs/cmd/pp/website/website-get.mdx +++ b/docs/docs/cmd/pp/website/website-get.mdx @@ -4,7 +4,7 @@ import TabItem from '@theme/TabItem'; # pp website get -Gets information about the specified Power Pages websites. +Gets information about the specified Power Pages website. ## Usage @@ -15,17 +15,17 @@ m365 pp website get [options] ## Options ```md definition-list -`--url [url]` +`-u, --url [url]` : The URL of the website to retrieve. Specify either `url`, `name` or `id`. `-n, --name [name]` : The name of the website to retrieve. Specify either `url`, `name` or `id`. `-i, --id [id]` -: The WebSiteId (GUID) of the website to retrieve. Specify either `url`, `name` or `id`. +: The WebSite Id (GUID) of the website to retrieve. Specify either `url`, `name` or `id`. `-e, --environmentName ` -: The name of the environment for which to retrieve Power Pages website from. +: The name of the environment from which to retrieve Power Pages website from. ``` diff --git a/src/m365/pp/commands/website/website-get.ts b/src/m365/pp/commands/website/website-get.ts index 746de80d69d..2b2783cd2d7 100644 --- a/src/m365/pp/commands/website/website-get.ts +++ b/src/m365/pp/commands/website/website-get.ts @@ -16,8 +16,7 @@ const options = globalOptionsZod ), id: zod.alias('i', z.string().uuid().optional()), name: zod.alias('n', z.string().optional()), - environmentName: zod.alias('e', z.string()), - asAdmin: z.boolean().default(false) + environmentName: zod.alias('e', z.string()) }).strict(); declare type Options = z.infer; @@ -44,7 +43,7 @@ class PpWebSiteGetCommand extends PowerPlatformCommand { public getRefinedSchema(schema: typeof options): z.ZodEffects | undefined { return schema - .refine(options => (options.url !== undefined || options.id !== undefined || options.name !== undefined) && !(options.url !== undefined && options.id !== undefined) && !(options.url !== undefined && options.name !== undefined) && !(options.id !== undefined && options.name !== undefined), { + .refine(options => [options.url, options.id, options.name].filter(x => x !== undefined).length === 1, { message: `Either url, id or name is required, but not multiple.` }); }