Skip to content

Commit

Permalink
Adds 'newClientSideComponentId' option to 'spo tenant applicationcust…
Browse files Browse the repository at this point in the history
…omizer set'. Closes #5063
  • Loading branch information
nanddeepn authored and martinlingstuyl committed Jul 6, 2023
1 parent 7826eaf commit 79e4138
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 14 deletions.
15 changes: 12 additions & 3 deletions docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-set.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ m365 spo tenant applicationcustomizer set [options]
`--newTitle [newTitle]`
: The updated title of the Application Customizer.

`--newClientSideComponentId [newClientSideComponentId]`
: The new Client Side Component Id (GUID) of the Application Customizer.

`-p, --clientSideComponentProperties [clientSideComponentProperties]`
: The Client Side Component properties of the Application Customizer.

Expand Down Expand Up @@ -55,7 +58,13 @@ When using the `--clientSideComponentProperties` option it's possible to enter a
Updates the title of an Application Customizer that is deployed as a tenant-wide extension by its id

```sh
m365 spo tenant applicationcustomizer set --id 3 --newTitle "Some customizer"
m365 spo tenant applicationcustomizer set --id 3 --newTitle "Some customizer"
```

Updates the Client Side Component Id of an Application Customizer that is deployed as a tenant-wide extension by its id

```sh
m365 spo tenant applicationcustomizer set --id 3 --newClientSideComponentId "b44a5182-9877-4029-baec-0181c70dacbc"
```

Updates the properties of an Application Customizer that is deployed as a tenant-wide extension by its id
Expand All @@ -67,13 +76,13 @@ m365 spo tenant applicationcustomizer set --id 3 --clientSideComponentProperties
Updates the title of an Application Customizer that is deployed as a tenant-wide extension by its title

```sh
m365 spo tenant applicationcustomizer set --title "Some customizer" --newTitle "Updated customizer"
m365 spo tenant applicationcustomizer set --title "Some customizer" --newTitle "Updated customizer"
```

Updates the title of an Application Customizer that is deployed as a tenant-wide extension by its clientSideComponentId

```sh
m365 spo tenant applicationcustomizer set --clientSideComponentId "7f8fd1f2-9d26-4a4a-a607-bf4622d7ec11" --newTitle "Some customizer"
m365 spo tenant applicationcustomizer set --clientSideComponentId "7f8fd1f2-9d26-4a4a-a607-bf4622d7ec11" --newTitle "Some customizer"
```

## Response
Expand Down
187 changes: 178 additions & 9 deletions src/m365/spo/commands/tenant/tenant-applicationcustomizer-set.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,30 @@ import Command, { CommandError } from '../../../../Command';
import request from '../../../../request';
import { telemetry } from '../../../../telemetry';
import { pid } from '../../../../utils/pid';
import { session } from '../../../../utils/session';
import { sinonUtil } from '../../../../utils/sinonUtil';
import commands from '../../commands';
import * as spoListItemListCommand from '../listitem/listitem-list';
import { urlUtil } from '../../../../utils/urlUtil';
import * as os from 'os';
import { session } from '../../../../utils/session';
const command: Command = require('./tenant-applicationcustomizer-set');

describe(commands.TENANT_APPLICATIONCUSTOMIZER_SET, () => {
const title = 'Some customizer';
const newTitle = 'New customizer';
const newClientSideComponentId = '7096cded-b83d-4eab-96f0-df477ed8c0bc';
const id = 3;
const clientSideComponentId = '7096cded-b83d-4eab-96f0-df477ed7c0bc';
const clientSideComponentProperties = '{ "someProperty": "Some value" }';
const webTemplate = "GROUP#0";
const spoUrl = 'https://contoso.sharepoint.com';
const appCatalogUrl = 'https://contoso.sharepoint.com/sites/apps';
const solutionId = 'ac555cb1-e5ac-409e-86dc-61e6651b1e66';
const clientComponentManifest = "{\"id\":\"6b2a54c5-3317-49eb-8621-1bbb76263629\",\"alias\":\"HelloWorldApplicationCustomizer\",\"componentType\":\"Extension\",\"extensionType\":\"ApplicationCustomizer\",\"version\":\"0.0.1\",\"manifestVersion\":2,\"loaderConfig\":{\"internalModuleBaseUrls\":[\"HTTPS://SPCLIENTSIDEASSETLIBRARY/\"],\"entryModuleId\":\"hello-world-application-customizer\",\"scriptResources\":{\"hello-world-application-customizer\":{\"type\":\"path\",\"path\":\"hello-world-application-customizer_b47769f9eca3d3b6c4d5.js\"},\"HelloWorldApplicationCustomizerStrings\":{\"type\":\"path\",\"path\":\"HelloWorldApplicationCustomizerStrings_en-us_72ca11838ac9bae2790a8692c260e1ac.js\"},\"@microsoft/sp-application-base\":{\"type\":\"component\",\"id\":\"4df9bb86-ab0a-4aab-ab5f-48bf167048fb\",\"version\":\"1.15.2\"},\"@microsoft/sp-core-library\":{\"type\":\"component\",\"id\":\"7263c7d0-1d6a-45ec-8d85-d4d1d234171b\",\"version\":\"1.15.2\"}}},\"mpnId\":\"Undefined-1.15.2\",\"clientComponentDeveloper\":\"\"}";
const solution = { "FileSystemObjectType": 0, "Id": 40, "ServerRedirectedEmbedUri": null, "ServerRedirectedEmbedUrl": "", "ClientComponentId": clientSideComponentId, "ClientComponentManifest": clientComponentManifest, "SolutionId": solutionId, "Created": "2022-11-03T11:25:17", "Modified": "2022-11-03T11:26:03" };
const solutionResponse = [solution];
const application = { "FileSystemObjectType": 0, "Id": 31, "ServerRedirectedEmbedUri": null, "ServerRedirectedEmbedUrl": "", "SkipFeatureDeployment": true, "ContainsTenantWideExtension": true, "Modified": "2022-11-03T11:26:03", "CheckoutUserId": null, "EditorId": 9 };
const applicationResponse = [application];
const applicationCustomizerResponse = {
value:
[{
Expand Down Expand Up @@ -93,6 +102,23 @@ describe(commands.TENANT_APPLICATIONCUSTOMIZER_SET, () => {
});
};

const postCallsStubClientSideComponentId = (): sinon.SinonStub => {
return sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `https://contoso.sharepoint.com/sites/apps/_api/web/GetList('%2Fsites%2Fapps%2Flists%2FTenantWideExtensions')/Items(3)/ValidateUpdateListItem()`) {
return {
value: [
{
FieldName: "TenantWideExtensionComponentId",
FieldValue: newClientSideComponentId
}
]
};
}

throw 'Invalid request';
});
};

before(() => {
cli = Cli.getInstance();
sinon.stub(auth, 'restoreAuth').resolves();
Expand Down Expand Up @@ -124,7 +150,9 @@ describe(commands.TENANT_APPLICATIONCUSTOMIZER_SET, () => {
sinonUtil.restore([
request.get,
request.post,
cli.getSettingWithDefaultValue
cli.getSettingWithDefaultValue,
Cli.executeCommand,
Cli.executeCommandWithOutput
]);
});

Expand All @@ -148,7 +176,12 @@ describe(commands.TENANT_APPLICATIONCUSTOMIZER_SET, () => {
});

it('fails validation if the clientSideComponentId is not a valid GUID', async () => {
const actual = await command.validate({ options: { clientSideComponentId: 'abc' } }, commandInfo);
const actual = await command.validate({ options: { clientSideComponentId: 'abc', newTitle: newTitle } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('fails validation if the newClientSideComponentId is not a valid GUID', async () => {
const actual = await command.validate({ options: { id: id, newClientSideComponentId: 'abc' } }, commandInfo);
assert.notStrictEqual(actual, true);
});

Expand All @@ -173,6 +206,11 @@ describe(commands.TENANT_APPLICATIONCUSTOMIZER_SET, () => {
assert.strictEqual(actual, true);
});

it('passes validation if newClientSideComponentId is valid', async () => {
const actual = await command.validate({ options: { id: id, newClientSideComponentId: newClientSideComponentId } }, commandInfo);
assert.strictEqual(actual, true);
});

it('handles error when tenant app catalog doesn\'t exist', async () => {
const errorMessage = 'No app catalog URL found';

Expand Down Expand Up @@ -295,36 +333,167 @@ describe(commands.TENANT_APPLICATIONCUSTOMIZER_SET, () => {
}), new CommandError(errorMessage));
});

it('Updates an application customizer by title', async () => {
it('updates title of an application customizer by title', async () => {
defaultGetCallStub("Title eq 'Some customizer'");
const executeCallsStub: sinon.SinonStub = defaultPostCallsStub();
await command.action(logger, {
options: {
title: title, newTitle: newTitle
}
});
assert(executeCallsStub.calledOnce);

assert.deepEqual(executeCallsStub.firstCall.args[0].data, { formValues: [{ FieldName: 'Title', FieldValue: 'New customizer' }] });
});

it('updates client side component id of an application customizer by title', async () => {
sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise<any> => {
if (command === spoListItemListCommand) {
if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/Lists/ComponentManifests`) {
return { 'stdout': JSON.stringify(solutionResponse) };
}
if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/AppCatalog`) {
return { 'stdout': JSON.stringify(applicationResponse) };
}
}

throw 'Invalid request';
});

defaultGetCallStub("Title eq 'Some customizer'");
const executeCallsStub: sinon.SinonStub = postCallsStubClientSideComponentId();
await command.action(logger, {
options: {
title: title, newClientSideComponentId: newClientSideComponentId
}
});
assert.deepEqual(executeCallsStub.firstCall.args[0].data, { formValues: [{ FieldName: 'TenantWideExtensionComponentId', FieldValue: '7096cded-b83d-4eab-96f0-df477ed8c0bc' }] });
});

it('Updates an application customizer by id', async () => {
it('updates properties of an application customizer by id', async () => {
defaultGetCallStub("Id eq '3'");
const executeCallsStub: sinon.SinonStub = defaultPostCallsStub();
await command.action(logger, {
options: {
id: id, clientSideComponentProperties: clientSideComponentProperties, verbose: true
}
});
assert(executeCallsStub.calledOnce);
assert.deepEqual(executeCallsStub.firstCall.args[0].data, { formValues: [{ FieldName: 'TenantWideExtensionComponentProperties', FieldValue: '{ "someProperty": "Some value" }' }] });
});

it('Updates an application customizer by clientSideComponentId', async () => {
it('updates an application customizer by clientSideComponentId', async () => {
defaultGetCallStub("TenantWideExtensionComponentId eq '7096cded-b83d-4eab-96f0-df477ed7c0bc'");
const executeCallsStub: sinon.SinonStub = defaultPostCallsStub();
await command.action(logger, {
options: {
clientSideComponentId: clientSideComponentId, webTemplate: webTemplate, verbose: true
}
});
assert(executeCallsStub.calledOnce);
assert.deepEqual(executeCallsStub.firstCall.args[0].data, { formValues: [{ FieldName: 'TenantWideExtensionWebTemplate', FieldValue: 'GROUP#0' }] });
});

it('throws an error when specific client side component is not found in manifest list', async () => {
sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise<any> => {
if (command === spoListItemListCommand) {
if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/Lists/ComponentManifests`) {
return { 'stdout': JSON.stringify([]) };
}
}

throw 'Invalid request';
});

defaultGetCallStub("Id eq '3'");
postCallsStubClientSideComponentId();

await assert.rejects(command.action(logger, { options: { id: id, newClientSideComponentId: newClientSideComponentId, verbose: true } }),
new CommandError('No component found with the specified clientSideComponentId found in the component manifest list. Make sure that the application is added to the application catalog'));
});

it('throws an error when client side component to update is not of type application customizer', async () => {
sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise<any> => {
if (command === spoListItemListCommand) {
if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/Lists/ComponentManifests`) {
const faultyClientComponentManifest = "{\"id\":\"6b2a54c5-3317-49eb-8621-1bbb76263629\",\"alias\":\"HelloWorldApplicationCustomizer\",\"componentType\":\"Extension\",\"extensionType\":\"FormCustomizer\",\"version\":\"0.0.1\",\"manifestVersion\":2,\"loaderConfig\":{\"internalModuleBaseUrls\":[\"HTTPS://SPCLIENTSIDEASSETLIBRARY/\"],\"entryModuleId\":\"hello-world-application-customizer\",\"scriptResources\":{\"hello-world-application-customizer\":{\"type\":\"path\",\"path\":\"hello-world-application-customizer_b47769f9eca3d3b6c4d5.js\"},\"HelloWorldApplicationCustomizerStrings\":{\"type\":\"path\",\"path\":\"HelloWorldApplicationCustomizerStrings_en-us_72ca11838ac9bae2790a8692c260e1ac.js\"},\"@microsoft/sp-application-base\":{\"type\":\"component\",\"id\":\"4df9bb86-ab0a-4aab-ab5f-48bf167048fb\",\"version\":\"1.15.2\"},\"@microsoft/sp-core-library\":{\"type\":\"component\",\"id\":\"7263c7d0-1d6a-45ec-8d85-d4d1d234171b\",\"version\":\"1.15.2\"}}},\"mpnId\":\"Undefined-1.15.2\",\"clientComponentDeveloper\":\"\"}";
const solutionDuplicate = { ...solution };
solutionDuplicate.ClientComponentManifest = faultyClientComponentManifest;
return { 'stdout': JSON.stringify([solutionDuplicate]) };
}
}

throw 'Invalid request';
});

defaultGetCallStub("Id eq '3'");
postCallsStubClientSideComponentId();

await assert.rejects(command.action(logger, { options: { id: id, newClientSideComponentId: newClientSideComponentId, verbose: true } }),
new CommandError(`The extension type of this component is not of type 'ApplicationCustomizer' but of type 'FormCustomizer'`));
});

it('throws an error when solution is not found in app catalog', async () => {
sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise<any> => {
if (command === spoListItemListCommand) {
if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/Lists/ComponentManifests`) {
return { 'stdout': JSON.stringify(solutionResponse) };
}
if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/AppCatalog`) {
return { 'stdout': JSON.stringify([]) };
}
}

throw 'Invalid request';
});

defaultGetCallStub("Id eq '3'");
postCallsStubClientSideComponentId();

await assert.rejects(command.action(logger, { options: { id: id, newClientSideComponentId: newClientSideComponentId, verbose: true } }),
new CommandError(`No component found with the solution id ${solutionId}. Make sure that the solution is available in the app catalog`));
});

it('throws an error when solution does not contain extension that can be deployed tenant-wide', async () => {
sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise<any> => {
if (command === spoListItemListCommand) {
if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/Lists/ComponentManifests`) {
return { 'stdout': JSON.stringify(solutionResponse) };
}
if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/AppCatalog`) {
const faultyApplication = { ...application };
faultyApplication.ContainsTenantWideExtension = false;
return { 'stdout': JSON.stringify([faultyApplication]) };
}
}

throw 'Invalid request';
});

defaultGetCallStub("Id eq '3'");
postCallsStubClientSideComponentId();

await assert.rejects(command.action(logger, { options: { id: id, newClientSideComponentId: newClientSideComponentId, verbose: true } }),
new CommandError(`The solution does not contain an extension that can be deployed to all sites. Make sure that you've entered the correct component Id.`));
});

it('throws an error when solution is not deployed globally', async () => {
sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise<any> => {
if (command === spoListItemListCommand) {
if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/Lists/ComponentManifests`) {
return { 'stdout': JSON.stringify(solutionResponse) };
}
if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/AppCatalog`) {
const faultyApplication = { ...application };
faultyApplication.SkipFeatureDeployment = false;
return { 'stdout': JSON.stringify([faultyApplication]) };
}
}

throw 'Invalid request';
});

defaultGetCallStub("Id eq '3'");
postCallsStubClientSideComponentId();

await assert.rejects(command.action(logger, { options: { id: id, newClientSideComponentId: newClientSideComponentId, verbose: true } }),
new CommandError(`The solution has not been deployed to all sites. Make sure to deploy this solution to all sites.`));
});
});
Loading

0 comments on commit 79e4138

Please sign in to comment.