diff --git a/src/cli/Cli.spec.ts b/src/cli/Cli.spec.ts index 47281a769a5..ae4086a44a2 100644 --- a/src/cli/Cli.spec.ts +++ b/src/cli/Cli.spec.ts @@ -8,7 +8,7 @@ import os from 'os'; import path from 'path'; import sinon from 'sinon'; import url from 'url'; -import Command, { CommandError } from '../Command.js'; +import Command, { CommandError, CommandErrorWithOutput } from '../Command.js'; import AnonymousCommand from '../m365/base/AnonymousCommand.js'; import cliCompletionUpdateCommand from '../m365/cli/commands/completion/completion-clink-update.js'; import { settingsNames } from '../settingsNames.js'; @@ -1275,14 +1275,14 @@ describe('Cli', () => { }); it('correctly handles error when executing command with output', (done) => { - sinon.stub(mockCommand, 'commandAction').callsFake(() => { throw 'Error'; }); + sinon.stub(mockCommand, 'commandAction').callsFake(() => { throw new CommandErrorWithOutput(new CommandError('Error')); }); Cli .executeCommandWithOutput(mockCommand, { options: { _: [] } }) .then(_ => { done('Command succeeded while expected fail'); }, e => { try { - assert.strictEqual(e.error, 'Error'); + assert.strictEqual(e.error.error.message, 'Error'); done(); } catch (e) { diff --git a/src/m365/spo/commands/tenant/tenant-appcatalog-add.spec.ts b/src/m365/spo/commands/tenant/tenant-appcatalog-add.spec.ts index 2aa808a313f..76d689e582d 100644 --- a/src/m365/spo/commands/tenant/tenant-appcatalog-add.spec.ts +++ b/src/m365/spo/commands/tenant/tenant-appcatalog-add.spec.ts @@ -4,22 +4,65 @@ 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, CommandErrorWithOutput } from '../../../../Command.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 spoSiteAddCommand from '../site/site-add.js'; -import spoSiteGetCommand from '../site/site-get.js'; -import spoSiteRemoveCommand from '../site/site-remove.js'; import command from './tenant-appcatalog-add.js'; -import spoTenantAppCatalogUrlGetCommand from './tenant-appcatalogurl-get.js'; +import { spo } from '../../../../utils/spo.js'; describe(commands.TENANT_APPCATALOG_ADD, () => { let log: any[]; let logger: Logger; let commandInfo: CommandInfo; + const siteResponse = { + AllowCreateDeclarativeWorkflow: true, + AllowDesigner: true, + AllowMasterPageEditing: false, + AllowRevertFromTemplate: false, + AllowSaveDeclarativeWorkflowAsTemplate: true, + AllowSavePublishDeclarativeWorkflow: true, + AllowSelfServiceUpgrade: true, + AllowSelfServiceUpgradeEvaluation: true, + AuditLogTrimmingRetention: 90, + Classification: '', + CompatibilityLevel: 15, + CurrentChangeToken: { + StringValue: '1;1;1a70e568-d286-4ad1-b036-734ff8667915;636527399616270000;66855110' + }, + DisableAppViews: false, + DisableCompanyWideSharingLinks: false, + DisableFlows: false, + ExternalSharingTipsEnabled: false, + GeoLocation: 'EUR', + GroupId: '7f5df2f4-9ed6-4df7-86d7-eefbfc4ab091', + HubSiteId: '00000000-0000-0000-0000-000000000000', + Id: '1a70e568-d286-4ad1-b036-734ff8667915', + IsHubSite: false, + LockIssue: null, + MaxItemsPerThrottledOperation: 5000, + NeedsB2BUpgrade: false, + ResourcePath: { + DecodedUrl: 'https://contoso.sharepoint.com/sites/appCatalog' + }, + PrimaryUri: 'https://contoso.sharepoint.com/sites/appCatalog', + ReadOnly: false, + RequiredDesignerVersion: '15.0.0.0', + SandboxedCodeActivationCapability: 2, + ServerRelativeUrl: '/sites/appCatalog', + ShareByEmailEnabled: true, + ShareByLinkEnabled: false, + ShowUrlStructure: false, + TrimAuditLog: true, + UIVersionConfigurationEnabled: false, + UpgradeReminderDate: '1899-12-30T00:00:00', + UpgradeScheduled: false, + UpgradeScheduledDate: '1753-01-01T00:00:00', + Upgrading: false, + Url: 'https://contoso.sharepoint.com/sites/appCatalog' + }; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -47,8 +90,10 @@ describe(commands.TENANT_APPCATALOG_ADD, () => { afterEach(() => { sinonUtil.restore([ - Cli.executeCommand, - Cli.executeCommandWithOutput + spo.removeSite, + spo.addSite, + spo.getSite, + spo.getTenantAppCatalogUrl ]); }); @@ -66,875 +111,294 @@ describe(commands.TENANT_APPCATALOG_ADD, () => { }); it('creates app catalog when app catalog and site with different URL already exist and force used', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog' || - args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog' || - args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').resolves(); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').resolves(siteResponse); await command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any); }); it('creates app catalog when app catalog and site with different URL already exist and force used (debug)', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog' || - args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return { - stdout: 'https://contoso.sharepoint.com/sites/old-app-catalog' - }; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog' || - args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').resolves(); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').resolves(siteResponse); await command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true, debug: true } } as any); }); it('handles error when creating app catalog when app catalog and site with different URL already exist and force used failed', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog' || - args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandError('An error has occurred'); - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog' || - args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').rejects(new Error('An error has occurred')); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').resolves(siteResponse); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any), new CommandError('An error has occurred')); }); it('handles error when app catalog and site with different URL already exist, force used and deleting the existing site failed', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandError('Error deleting site new-app-catalog'); - } - - throw new CommandError('Invalid URL'); - } - - if (command === spoSiteAddCommand) { - throw 'Should not be called'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog' || - args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').rejects(new Error('Error deleting site new-app-catalog')); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').resolves(siteResponse); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any), new CommandError('Error deleting site new-app-catalog')); }); it('creates app catalog when app catalog already exists, site with different URL does not exist and force used', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw 'Invalid URL'; + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').resolves(); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').callsFake(async (url) => { + if (url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { + return siteResponse; } - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; + if (url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { + throw new Error('404 FILE NOT FOUND'); } - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; + throw 'Invalid request'; }); await command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any); }); it('creates app catalog when app catalog already exists, site with different URL does not exist and force used (debug)', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').resolves(); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').callsFake(async (url) => { + if (url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { + return siteResponse; } - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw 'Invalid URL'; + if (url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { + throw new Error('404 FILE NOT FOUND'); } - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; + throw 'Invalid request'; }); await command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true, debug: true } } as any); }); it('handles error when creating app catalog when app catalog already exists, site with different URL does not exist and force used', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').rejects(new Error('An error has occurred')); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').callsFake(async (url) => { + if (url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { + return siteResponse; } - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandError('An error has occurred'); - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - - throw new CommandError('Invalid URL'); + if (url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { + throw new Error('404 FILE NOT FOUND'); } - throw 'Unknown case'; + throw 'Invalid request'; }); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true, debug: true } } as any), new CommandError('An error has occurred')); }); it('handles error when retrieving site with different URL failed and app catalog already exists, and force used', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').callsFake(async (url) => { + if (url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { + return siteResponse; } - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; + if (url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { + throw new Error('An error has occurred'); } - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('An error has occurred')); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; + throw 'Invalid request'; }); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true, debug: true } } as any), new CommandError('An error has occurred')); }); it('handles error when deleting existing app catalog failed', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - throw new CommandError('An error has occurred'); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return { - stdout: 'https://contoso.sharepoint.com/sites/old-app-catalog' - }; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'removeSite').rejects(new Error('An error has occurred')); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').resolves(siteResponse); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any), new CommandError('An error has occurred')); }); it('handles error app catalog exists and no force used', async () => { - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return { - stdout: 'https://contoso.sharepoint.com/sites/old-app-catalog' - }; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').resolves(siteResponse); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog' } } as any), new CommandError('Another site exists at https://contoso.sharepoint.com/sites/old-app-catalog')); }); it('creates app catalog when app catalog does not exist, site with different URL already exists and force used', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').resolves(); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').callsFake(async (url) => { + if (url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { + throw new Error('404 FILE NOT FOUND'); } - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); + if (url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { + return siteResponse; } - throw 'Unknown case'; + throw 'Invalid request'; }); await command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any); }); it('handles error when creating app catalog when app catalog does not exist, site with different URL already exists and force used', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandError('An error has occurred'); - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').rejects(new Error('An error has occurred')); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').callsFake(async (url) => { + if (url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { + throw new Error('404 FILE NOT FOUND'); } - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); + if (url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { + return siteResponse; } - throw 'Unknown case'; + throw 'Invalid request'; }); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any), new CommandError('An error has occurred')); }); it('handles error when deleting existing site, when app catalog does not exist, site with different URL already exists and force used', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandError('An error has occurred'); - } - - throw new CommandError('Invalid URL'); + sinon.stub(spo, 'removeSite').rejects(new Error('An error has occurred')); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').callsFake(async (url) => { + if (url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { + throw new Error('404 FILE NOT FOUND'); } - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); + if (url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { + return siteResponse; } - throw 'Unknown case'; + throw 'Invalid request'; }); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any), new CommandError('An error has occurred')); }); it('handles error when app catalog does not exist, site with different URL already exists and force not used', async () => { - sinon.stub(Cli, 'executeCommand').callsFake(() => { - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').callsFake(async (url) => { + if (url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { + throw new Error('404 FILE NOT FOUND'); } - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); + if (url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { + return siteResponse; } - throw 'Unknown case'; + throw 'Invalid request'; }); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog' } } as any), new CommandError('Another site exists at https://contoso.sharepoint.com/sites/new-app-catalog')); }); it(`creates app catalog when app catalog and site with different URL don't exist`, async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog' || - args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'addSite').resolves(); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').rejects(new Error('404 FILE NOT FOUND')); await command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog' } } as any); }); it(`handles error when creating app catalog fails, when app catalog when app catalog does and site with different URL don't exist`, async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandError('An error has occurred'); - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return 'https://contoso.sharepoint.com/sites/old-app-catalog'; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog' || - args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'addSite').rejects(new Error('An error has occurred')); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').rejects(new Error('404 FILE NOT FOUND')); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog' } } as any), new CommandError('An error has occurred')); }); it(`handles error when checking if the app catalog site exists`, async () => { - sinon.stub(Cli, 'executeCommand').callsFake(() => { - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return { - stdout: 'https://contoso.sharepoint.com/sites/old-app-catalog' - }; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/old-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('An error has occurred')); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/old-app-catalog'); + sinon.stub(spo, 'getSite').rejects(new Error('An error has occurred')); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog' } } as any), new CommandError('An error has occurred')); }); it(`creates app catalog when app catalog not registered, site with different URL exists and force used`, async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return ''; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').resolves(); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves(''); + sinon.stub(spo, 'getSite').resolves(siteResponse); await command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any); }); it(`creates app catalog when app catalog not registered, site with different URL exists and force used (debug)`, async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return ''; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').resolves(); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves(''); + sinon.stub(spo, 'getSite').resolves(siteResponse); await command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true, debug: true } } as any); }); it(`handles error when creating app catalog when app catalog not registered, site with different URL exists and force used`, async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandError('An error has occurred'); - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return ''; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'removeSite').resolves(); + sinon.stub(spo, 'addSite').rejects(new Error('An error has occurred')); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves(''); + sinon.stub(spo, 'getSite').resolves(siteResponse); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any), new CommandError('An error has occurred')); }); it(`handles error when deleting existing site when app catalog not registered, site with different URL exists and force used`, async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteRemoveCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandError('An error has occurred'); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return ''; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'removeSite').rejects(new Error('An error has occurred')); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves(''); + sinon.stub(spo, 'getSite').resolves(siteResponse); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog', force: true } } as any), new CommandError('An error has occurred')); }); it(`handles error when app catalog not registered, site with different URL exists and force not used`, async () => { - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return ''; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves(''); + sinon.stub(spo, 'getSite').resolves(siteResponse); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog' } } as any), new CommandError('Another site exists at https://contoso.sharepoint.com/sites/new-app-catalog')); }); it(`creates app catalog when app catalog not registered and site with different URL doesn't exist`, async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - return; - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return ''; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'addSite').resolves(); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves(''); + sinon.stub(spo, 'getSite').rejects(new Error('404 FILE NOT FOUND')); await command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog' } } as any); }); it(`handles error when creating app catalog when app catalog not registered and site with different URL doesn't exist`, async () => { - sinon.stub(Cli, 'executeCommand').callsFake(async (command, args) => { - if (command === spoSiteAddCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandError('An error has occurred'); - } - - throw 'Invalid URL'; - } - - throw 'Unknown case'; - }); - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return ''; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('404 FILE NOT FOUND')); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'addSite').rejects(new Error('An error has occurred')); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves(''); + sinon.stub(spo, 'getSite').rejects(new Error('404 FILE NOT FOUND')); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog' } } as any), new CommandError('An error has occurred')); }); it(`handles error when app catalog not registered and checking if the site with different URL exists throws error`, async () => { - sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - return ''; - } - - if (command === spoSiteGetCommand) { - if (args.options.url === 'https://contoso.sharepoint.com/sites/new-app-catalog') { - throw new CommandErrorWithOutput(new CommandError('An error has occurred')); - } - - throw new CommandError('Invalid URL'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'getTenantAppCatalogUrl').resolves(''); + sinon.stub(spo, 'getSite').rejects(new Error('An error has occurred')); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog' } } as any), new CommandError('An error has occurred')); }); it(`handles error when checking if app catalog registered throws error`, async () => { - sinon.stub(Cli, 'executeCommandWithOutput').callsFake((command): Promise => { - if (command === spoTenantAppCatalogUrlGetCommand) { - throw new CommandError('An error has occurred'); - } - - throw 'Unknown case'; - }); + sinon.stub(spo, 'getTenantAppCatalogUrl').rejects(new Error('An error has occurred')); await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com/sites/new-app-catalog' } } as any), new CommandError('An error has occurred')); }); diff --git a/src/m365/spo/commands/tenant/tenant-appcatalog-add.ts b/src/m365/spo/commands/tenant/tenant-appcatalog-add.ts index 90de2fd7442..f588fdf42bb 100644 --- a/src/m365/spo/commands/tenant/tenant-appcatalog-add.ts +++ b/src/m365/spo/commands/tenant/tenant-appcatalog-add.ts @@ -1,14 +1,10 @@ -import { Cli, CommandOutput } from '../../../../cli/Cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import Command, { CommandError } from '../../../../Command.js'; +import { CommandError } from '../../../../Command.js'; import GlobalOptions from '../../../../GlobalOptions.js'; +import { spo } from '../../../../utils/spo.js'; import { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; import commands from '../../commands.js'; -import spoSiteAddCommand, { Options as SpoSiteAddCommandOptions } from '../site/site-add.js'; -import spoSiteGetCommand from '../site/site-get.js'; -import spoSiteRemoveCommand from '../site/site-remove.js'; -import spoTenantAppCatalogUrlGetCommand from './tenant-appcatalogurl-get.js'; interface CommandArgs { options: Options; @@ -89,24 +85,27 @@ class SpoTenantAppCatalogAddCommand extends SpoCommand { if (this.verbose) { await logger.logToStderr('Checking for existing app catalog URL...'); } + try { + const appCatalogUrl: string | null = await spo.getTenantAppCatalogUrl(logger, this.verbose); + if (!appCatalogUrl) { + if (this.verbose) { + logger.logToStderr('No app catalog URL found'); + } + } + else { + if (this.verbose) { + logger.logToStderr(`Found app catalog URL ${appCatalogUrl}`); + } - const spoTenantAppCatalogUrlGetCommandOutput: CommandOutput = await Cli.executeCommandWithOutput(spoTenantAppCatalogUrlGetCommand as Command, { options: { output: 'text', _: [] } }); - const appCatalogUrl: string | undefined = spoTenantAppCatalogUrlGetCommandOutput.stdout; - if (!appCatalogUrl) { - if (this.verbose) { - await logger.logToStderr('No app catalog URL found'); + //Using JSON.parse + await this.ensureNoExistingSite(appCatalogUrl, args.options.force, logger); } + await this.ensureNoExistingSite(args.options.url, args.options.force, logger); + await this.createAppCatalog(args.options, logger); } - else { - if (this.verbose) { - await logger.logToStderr(`Found app catalog URL ${appCatalogUrl}`); - } - - //Using JSON.parse - await this.ensureNoExistingSite(appCatalogUrl, args.options.force, logger); + catch (ex) { + this.handleRejectedODataPromise(ex); } - await this.ensureNoExistingSite(args.options.url, args.options.force, logger); - await this.createAppCatalog(args.options, logger); } private async ensureNoExistingSite(url: string, force: boolean, logger: Logger): Promise { @@ -114,17 +113,8 @@ class SpoTenantAppCatalogAddCommand extends SpoCommand { await logger.logToStderr(`Checking if site ${url} exists...`); } - const siteGetOptions = { - options: { - url: url, - verbose: this.verbose, - debug: this.debug, - _: [] - } - }; - try { - await Cli.executeCommandWithOutput(spoSiteGetCommand as Command, siteGetOptions); + await spo.getSite(url, logger, this.verbose); if (this.verbose) { await logger.logToStderr(`Found site ${url}`); @@ -138,20 +128,12 @@ class SpoTenantAppCatalogAddCommand extends SpoCommand { await logger.logToStderr(`Deleting site ${url}...`); } - const siteRemoveOptions = { - url: url, - skipRecycleBin: true, - wait: true, - confirm: true, - verbose: this.verbose, - debug: this.debug - }; - - await Cli.executeCommand(spoSiteRemoveCommand as Command, { options: { ...siteRemoveOptions, _: [] } }); + await spo.removeSite(url, true, true, logger, this.verbose); } catch (err: any) { - if (err.error?.message !== 'File not Found' && err.error?.message !== '404 FILE NOT FOUND') { - throw err.error || err; + logger.log(err); + if (err.message !== 'File not Found' && err.message !== '404 FILE NOT FOUND') { + throw err.message; } if (this.verbose) { @@ -167,19 +149,25 @@ class SpoTenantAppCatalogAddCommand extends SpoCommand { await logger.logToStderr(`Creating app catalog at ${options.url}...`); } - const siteAddOptions = { - webTemplate: 'APPCATALOG#0', - title: 'App catalog', - type: 'ClassicSite', - url: options.url, - timeZone: options.timeZone, - owners: options.owner, - wait: options.wait, - verbose: this.verbose, - debug: this.debug, - removeDeletedSite: false - } as SpoSiteAddCommandOptions; - return Cli.executeCommand(spoSiteAddCommand as Command, { options: { ...siteAddOptions, _: [] } }); + return await spo.addSite( + 'App catalog', + logger, + this.verbose, + options.wait, + 'ClassicSite', + undefined, + undefined, + options.owner, + undefined, + false, + undefined, + undefined, + undefined, + options.url, + undefined, + undefined, + options.timeZone, + 'APPCATALOG#0'); } } diff --git a/src/utils/aadGroup.spec.ts b/src/utils/aadGroup.spec.ts index c270c3cb1a8..8b1936ae3c6 100644 --- a/src/utils/aadGroup.spec.ts +++ b/src/utils/aadGroup.spec.ts @@ -170,4 +170,21 @@ describe('utils/aadGroup', () => { await aadGroup.setGroup(validGroupId, false, logger, true); assert(patchStub.called); }); + + it('removes a Microsoft 365 group', async () => { + const deleteStub = sinon.stub(request, 'delete').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${validGroupId}`) { + return { + value: [ + { id: singleGroupResponse.id } + ] + }; + } + + return 'Invalid Request'; + }); + + await aadGroup.removeGroup(validGroupId, logger, true); + assert(deleteStub.called); + }); }); \ No newline at end of file diff --git a/src/utils/aadGroup.ts b/src/utils/aadGroup.ts index eed15684bfd..3790f9126ad 100644 --- a/src/utils/aadGroup.ts +++ b/src/utils/aadGroup.ts @@ -91,5 +91,26 @@ export const aadGroup = { }; await request.patch(requestOptions); + }, + + /** + * Removes a Microsoft 365 group + * @param groupId The id of the group. + * @param logger the Logger object + * @param verbose set if verbose logging should be logged + */ + async removeGroup(groupId: string, logger: Logger, verbose: boolean): Promise { + if (verbose) { + logger.logToStderr(`Removing Microsoft 365 Group: ${groupId}...`); + } + + const requestOptions: any = { + url: `https://graph.microsoft.com/v1.0/groups/${groupId}`, + headers: { + 'accept': 'application/json;odata.metadata=none' + } + }; + + request.delete(requestOptions); } }; \ No newline at end of file diff --git a/src/utils/spo.spec.ts b/src/utils/spo.spec.ts index e31be7affba..a948459cbc3 100644 --- a/src/utils/spo.spec.ts +++ b/src/utils/spo.spec.ts @@ -79,6 +79,9 @@ describe('utils/spo', () => { spo.siteExistsInTheRecycleBin, spo.getSpoUrl, spo.getTenantId, + spo.deleteSiteFromTheRecycleBin, + spo.deleteSite, + aadGroup.getGroupById, global.setTimeout ]); auth.service.spoUrl = undefined; @@ -2016,4 +2019,421 @@ describe('utils/spo', () => { const actual = await spo.getWeb('https://contoso.sharepoint.com', logger, true); assert.deepStrictEqual(actual, webResponse); }); + + it('successfully retrieves a site', async () => { + const sitePropertiesResponse = { + AllowCreateDeclarativeWorkflow: true, + AllowDesigner: true, + AllowMasterPageEditing: false, + AllowRevertFromTemplate: false, + AllowSaveDeclarativeWorkflowAsTemplate: true, + AllowSavePublishDeclarativeWorkflow: true, + AllowSelfServiceUpgrade: true, + AllowSelfServiceUpgradeEvaluation: true, + AuditLogTrimmingRetention: 90, + Classification: '', + CompatibilityLevel: 15, + CurrentChangeToken: { + StringValue: '1;1;1a70e568-d286-4ad1-b036-734ff8667915;636527399616270000;66855110' + }, + DisableAppViews: false, + DisableCompanyWideSharingLinks: false, + DisableFlows: false, + ExternalSharingTipsEnabled: false, + GeoLocation: 'EUR', + GroupId: '7f5df2f4-9ed6-4df7-86d7-eefbfc4ab091', + HubSiteId: '00000000-0000-0000-0000-000000000000', + Id: '1a70e568-d286-4ad1-b036-734ff8667915', + IsHubSite: false, + LockIssue: null, + MaxItemsPerThrottledOperation: 5000, + NeedsB2BUpgrade: false, + ResourcePath: { + DecodedUrl: 'https://contoso.sharepoint.com/sites/sales' + }, + PrimaryUri: 'https://contoso.sharepoint.com/sites/sales', + ReadOnly: false, + RequiredDesignerVersion: '15.0.0.0', + SandboxedCodeActivationCapability: 2, + ServerRelativeUrl: '/sites/sales', + ShareByEmailEnabled: true, + ShareByLinkEnabled: false, + ShowUrlStructure: false, + TrimAuditLog: true, + UIVersionConfigurationEnabled: false, + UpgradeReminderDate: '1899-12-30T00:00:00', + UpgradeScheduled: false, + UpgradeScheduledDate: '1753-01-01T00:00:00', + Upgrading: false, + Url: 'https://contoso.sharepoint.com/sites/sales' + }; + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/sales/_api/site`) { + return sitePropertiesResponse; + } + + throw 'invalid request'; + }); + + const actual = await spo.getSite('https://contoso.sharepoint.com/sites/sales', logger, true); + assert.deepStrictEqual(actual, sitePropertiesResponse); + }); + + it('deletes a site succesfully and waits for completion', async () => { + sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) { + if (opts.data === `https://contoso.sharepoint.com/sites/demosite`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.7324.1200", "ErrorInfo": null, "TraceCorrelationId": "e13c489e-304e-5000-8242-705e26a87302" + }, 185, { + "IsNull": false + }, 186, { + "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.SpoOperation", "_ObjectIdentity_": "e13c489e-304e-5000-8242-705e26a87302|908bed80-a04a-4433-b4a0-883d9847d110:67753f63-bc14-4012-869e-f808a43fe023\nSpoOperation\nRemoveSite\n636536266495764941\nhttps%3a%2f%2fcontoso.sharepoint.com%2fsites%2fdemosite\n00000000-0000-0000-0000-000000000000", "IsComplete": false, "PollingInterval": 15000 + } + ]); + } + + if (opts.data === ``) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.8015.1210", "ErrorInfo": null, "TraceCorrelationId": "5eda879e-90d5-6000-d611-e6bfd5acde9f" + }, 12, { + "IsNull": false + }, 14, { + "IsNull": false + }, 15, { + "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.Tenant", "_ObjectIdentity_": "5eda879e-90d5-6000-d611-e6bfd5acde9f|908bed80-a04a-4433-b4a0-883d9847d110:2ca3eaa5-140f-4175-9563-1172edf9f339\nTenant", "AllowDownloadingNonWebViewableFiles": true, "AllowedDomainListForSyncClient": [ + + ], "AllowEditing": true, "AllowLimitedAccessOnUnmanagedDevices": false, "ApplyAppEnforcedRestrictionsToAdHocRecipients": true, "BccExternalSharingInvitations": false, "BccExternalSharingInvitationsList": null, "BlockAccessOnUnmanagedDevices": false, "BlockDownloadOfAllFilesForGuests": false, "BlockDownloadOfAllFilesOnUnmanagedDevices": false, "BlockDownloadOfViewableFilesForGuests": false, "BlockDownloadOfViewableFilesOnUnmanagedDevices": false, "BlockMacSync": false, "CommentsOnSitePagesDisabled": false, "CompatibilityRange": "15,15", "ConditionalAccessPolicy": 0, "DefaultLinkPermission": 2, "DefaultSharingLinkType": 2, "DisabledWebPartIds": null, "DisableReportProblemDialog": false, "DisallowInfectedFileDownload": false, "DisplayNamesOfFileViewers": true, "DisplayStartASiteOption": false, "EmailAttestationReAuthDays": 30, "EmailAttestationRequired": false, "EnableGuestSignInAcceleration": false, "EnableMinimumVersionRequirement": true, "ExcludedFileExtensionsForSyncClient": [ + "" + ], "ExternalServicesEnabled": true, "FileAnonymousLinkType": 1, "FilePickerExternalImageSearchEnabled": true, "FolderAnonymousLinkType": 1, "HideSyncButtonOnODB": false, "IPAddressAllowList": "", "IPAddressEnforcement": false, "IPAddressWACTokenLifetime": 15, "IsHubSitesMultiGeoFlightEnabled": false, "IsMultiGeo": false, "IsUnmanagedSyncClientForTenantRestricted": false, "IsUnmanagedSyncClientRestrictionFlightEnabled": true, "LegacyAuthProtocolsEnabled": true, "LimitedAccessFileType": 1, "NoAccessRedirectUrl": null, "NotificationsInOneDriveForBusinessEnabled": true, "NotificationsInSharePointEnabled": true, "NotifyOwnersWhenInvitationsAccepted": true, "NotifyOwnersWhenItemsReshared": true, "ODBAccessRequests": 0, "ODBMembersCanShare": 0, "OfficeClientADALDisabled": false, "OneDriveForGuestsEnabled": false, "OneDriveStorageQuota": 5242880, "OptOutOfGrooveBlock": false, "OptOutOfGrooveSoftBlock": false, "OrphanedPersonalSitesRetentionPeriod": 30, "OwnerAnonymousNotification": true, "PermissiveBrowserFileHandlingOverride": false, "PreventExternalUsersFromResharing": false, "ProvisionSharedWithEveryoneFolder": false, "PublicCdnAllowedFileTypes": "CSS,EOT,GIF,ICO,JPEG,JPG,JS,MAP,PNG,SVG,TTF,WOFF", "PublicCdnEnabled": false, "PublicCdnOrigins": [ + + ], "RequireAcceptingAccountMatchInvitedAccount": false, "RequireAnonymousLinksExpireInDays": -1, "ResourceQuota": 6300, "ResourceQuotaAllocated": 1200, "RootSiteUrl": "https:\u002f\u002fcontoso.sharepoint.com", "SearchResolveExactEmailOrUPN": false, "SharingAllowedDomainList": null, "SharingBlockedDomainList": null, "SharingCapability": 2, "SharingDomainRestrictionMode": 0, "ShowAllUsersClaim": false, "ShowEveryoneClaim": false, "ShowEveryoneExceptExternalUsersClaim": true, "ShowNGSCDialogForSyncOnODB": true, "ShowPeoplePickerSuggestionsForGuestUsers": false, "SignInAccelerationDomain": "", "SocialBarOnSitePagesDisabled": false, "SpecialCharactersStateInFileFolderNames": 1, "StartASiteFormUrl": null, "StorageQuota": 1355776, "StorageQuotaAllocated": 135266304, "SyncPrivacyProfileProperties": true, "UseFindPeopleInPeoplePicker": false, "UsePersistentCookiesForExplorerView": false, "UserVoiceForFeedbackEnabled": true + }, 16, { + "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.SpoOperation", "_ObjectIdentity_": "5eda879e-90d5-6000-d611-e6bfd5acde9f|908bed80-a04a-4433-b4a0-883d9847d110:2ca3eaa5-140f-4175-9563-1172edf9f339\nSpoOperation\nRemoveSite\n636707032254311675\nhttps%3a%2f%2fcontoso.sharepoint.com%2fsites%2fdemosite\n00000000-0000-0000-0000-000000000000", "IsComplete": true, "PollingInterval": 15000 + } + ]); + } + } + + throw 'invalid request'; + }); + + sinon.stub(global, 'setTimeout').callsFake((fn) => { + fn(); + return {} as any; + }); + + await spo.deleteSite('https://contoso-admin.sharepoint.com', 'https://contoso.sharepoint.com/sites/demosite', true, logger, true); + assert(postStub.called); + }); + + it('deletes a site succesfully without waiting', async () => { + sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) { + if (opts.data === `https://contoso.sharepoint.com/sites/demosite`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.8015.1210", "ErrorInfo": null, "TraceCorrelationId": "5eda879e-90d5-6000-d611-e6bfd5acde9f" + }, 12, { + "IsNull": false + }, 14, { + "IsNull": false + }, 15, { + "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.Tenant", "_ObjectIdentity_": "5eda879e-90d5-6000-d611-e6bfd5acde9f|908bed80-a04a-4433-b4a0-883d9847d110:2ca3eaa5-140f-4175-9563-1172edf9f339\nTenant", "AllowDownloadingNonWebViewableFiles": true, "AllowedDomainListForSyncClient": [ + + ], "AllowEditing": true, "AllowLimitedAccessOnUnmanagedDevices": false, "ApplyAppEnforcedRestrictionsToAdHocRecipients": true, "BccExternalSharingInvitations": false, "BccExternalSharingInvitationsList": null, "BlockAccessOnUnmanagedDevices": false, "BlockDownloadOfAllFilesForGuests": false, "BlockDownloadOfAllFilesOnUnmanagedDevices": false, "BlockDownloadOfViewableFilesForGuests": false, "BlockDownloadOfViewableFilesOnUnmanagedDevices": false, "BlockMacSync": false, "CommentsOnSitePagesDisabled": false, "CompatibilityRange": "15,15", "ConditionalAccessPolicy": 0, "DefaultLinkPermission": 2, "DefaultSharingLinkType": 2, "DisabledWebPartIds": null, "DisableReportProblemDialog": false, "DisallowInfectedFileDownload": false, "DisplayNamesOfFileViewers": true, "DisplayStartASiteOption": false, "EmailAttestationReAuthDays": 30, "EmailAttestationRequired": false, "EnableGuestSignInAcceleration": false, "EnableMinimumVersionRequirement": true, "ExcludedFileExtensionsForSyncClient": [ + "" + ], "ExternalServicesEnabled": true, "FileAnonymousLinkType": 1, "FilePickerExternalImageSearchEnabled": true, "FolderAnonymousLinkType": 1, "HideSyncButtonOnODB": false, "IPAddressAllowList": "", "IPAddressEnforcement": false, "IPAddressWACTokenLifetime": 15, "IsHubSitesMultiGeoFlightEnabled": false, "IsMultiGeo": false, "IsUnmanagedSyncClientForTenantRestricted": false, "IsUnmanagedSyncClientRestrictionFlightEnabled": true, "LegacyAuthProtocolsEnabled": true, "LimitedAccessFileType": 1, "NoAccessRedirectUrl": null, "NotificationsInOneDriveForBusinessEnabled": true, "NotificationsInSharePointEnabled": true, "NotifyOwnersWhenInvitationsAccepted": true, "NotifyOwnersWhenItemsReshared": true, "ODBAccessRequests": 0, "ODBMembersCanShare": 0, "OfficeClientADALDisabled": false, "OneDriveForGuestsEnabled": false, "OneDriveStorageQuota": 5242880, "OptOutOfGrooveBlock": false, "OptOutOfGrooveSoftBlock": false, "OrphanedPersonalSitesRetentionPeriod": 30, "OwnerAnonymousNotification": true, "PermissiveBrowserFileHandlingOverride": false, "PreventExternalUsersFromResharing": false, "ProvisionSharedWithEveryoneFolder": false, "PublicCdnAllowedFileTypes": "CSS,EOT,GIF,ICO,JPEG,JPG,JS,MAP,PNG,SVG,TTF,WOFF", "PublicCdnEnabled": false, "PublicCdnOrigins": [ + + ], "RequireAcceptingAccountMatchInvitedAccount": false, "RequireAnonymousLinksExpireInDays": -1, "ResourceQuota": 6300, "ResourceQuotaAllocated": 1200, "RootSiteUrl": "https:\u002f\u002fcontoso.sharepoint.com", "SearchResolveExactEmailOrUPN": false, "SharingAllowedDomainList": null, "SharingBlockedDomainList": null, "SharingCapability": 2, "SharingDomainRestrictionMode": 0, "ShowAllUsersClaim": false, "ShowEveryoneClaim": false, "ShowEveryoneExceptExternalUsersClaim": true, "ShowNGSCDialogForSyncOnODB": true, "ShowPeoplePickerSuggestionsForGuestUsers": false, "SignInAccelerationDomain": "", "SocialBarOnSitePagesDisabled": false, "SpecialCharactersStateInFileFolderNames": 1, "StartASiteFormUrl": null, "StorageQuota": 1355776, "StorageQuotaAllocated": 135266304, "SyncPrivacyProfileProperties": true, "UseFindPeopleInPeoplePicker": false, "UsePersistentCookiesForExplorerView": false, "UserVoiceForFeedbackEnabled": true + }, 16, { + "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.SpoOperation", "_ObjectIdentity_": "5eda879e-90d5-6000-d611-e6bfd5acde9f|908bed80-a04a-4433-b4a0-883d9847d110:2ca3eaa5-140f-4175-9563-1172edf9f339\nSpoOperation\nRemoveSite\n636707032254311675\nhttps%3a%2f%2fcontoso.sharepoint.com%2fsites%2fdemosite\n00000000-0000-0000-0000-000000000000", "IsComplete": true, "PollingInterval": 15000 + } + ]); + } + } + + throw 'invalid request'; + }); + + await spo.deleteSite('https://contoso-admin.sharepoint.com', 'https://contoso.sharepoint.com/sites/demosite', true, logger, true); + assert(postStub.called); + }); + + it('handles error while deleting a site', async () => { + sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' }); + + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) { + if (opts.data === `https://contoso.sharepoint.com/sites/demosite`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.7324.1200", "ErrorInfo": { + "ErrorMessage": "An error has occurred.", "ErrorValue": null, "TraceCorrelationId": "e13c489e-2026-5000-8242-7ec96d02ba1d", "ErrorCode": -1, "ErrorTypeName": "SPException" + }, "TraceCorrelationId": "e13c489e-2026-5000-8242-7ec96d02ba1d" + } + ]); + } + } + + throw 'invalid request'; + }); + + try { + await spo.deleteSite('https://contoso-admin.sharepoint.com', 'https://contoso.sharepoint.com/sites/demosite', true, logger, true); + assert.fail('No error message thrown.'); + } + catch (ex) { + assert.deepStrictEqual(ex, 'An error has occurred.'); + } + }); + + it('removes a site that is not connected to a group succesfully and removes from recycle bin', async () => { + sinon.stub(spo, 'getSpoAdminUrl').resolves('https://contoso-admin.sharepoint.com'); + sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' }); + sinon.stub(spo, 'deleteSite').resolves(); + sinon.stub(spo, 'deleteSiteFromTheRecycleBin').resolves(); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) { + if (opts.data === `https://contoso.sharepoint.com`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.20530.12001", + "ErrorInfo": null, + "TraceCorrelationId": "10f1829f-d000-0000-5962-1110d33e2cf2" + }, + 4, + { + "IsNull": false + }, + 5, + { + "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.SiteProperties", + "_ObjectIdentity_": "10f1829f-d000-0000-5962-1110d33e2cf2|908bed80-a04a-4433-b4a0-883d9847d110:095efa67-57fa-40c7-b7cc-e96dc3e5780c\nSiteProperties\nhttps%3a%2f%2fcontoso.sharepoint.com%2fsites%2fdemosite", + "GroupId": "/Guid(00000000-0000-0000-0000-000000000000)/" + } + ]); + } + } + + throw 'invalid request'; + }); + + await spo.removeSite('https://contoso.sharepoint.com', true, true, logger, true); + assert(postStub.called); + }); + + it('handles exception while getting site properties by url while trying to remove a site', async () => { + sinon.stub(spo, 'getSpoAdminUrl').resolves('https://contoso-admin.sharepoint.com'); + sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' }); + + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) { + if (opts.data === `https://contoso.sharepoint.com`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.20530.12001", + "ErrorInfo": { + "ErrorMessage": "Cannot get site https://contoso.sharepoint.com.", + "ErrorValue": null, + "TraceCorrelationId": "3929839f-9018-0000-5518-a12b0af612a8", + "ErrorCode": -1, + "ErrorTypeName": "Microsoft.Online.SharePoint.Common.SpoNoSiteException" + }, + "TraceCorrelationId": "3929839f-9018-0000-5518-a12b0af612a8" + } + ]); + } + } + + throw 'invalid request'; + }); + + try { + await spo.removeSite('https://contoso.sharepoint.com', true, true, logger, true); + assert.fail('No error message thrown.'); + } + catch (ex) { + assert.deepStrictEqual(ex, 'Cannot get site https://contoso.sharepoint.com.'); + } + }); + + it('removes a site that is connected to a group succesfully', async () => { + sinon.stub(spo, 'getSpoAdminUrl').resolves('https://contoso-admin.sharepoint.com'); + sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' }); + sinon.stub(aadGroup, 'removeGroup').resolves(); + sinon.stub(spo, 'deleteSite').resolves(); + sinon.stub(aadGroup, 'getGroupById').resolves({ + id: '58587cc9-560c-4adb-a849-e669bd37c5f8', + deletedDateTime: null, + classification: null, + createdDateTime: '2017-11-29T03:27:05Z', + description: 'This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.', + displayName: 'Finance', + groupTypes: [ + 'Unified' + ], + mail: 'finance@contoso.onmicrosoft.com', + mailEnabled: true, + mailNickname: 'finance', + onPremisesLastSyncDateTime: null, + onPremisesProvisioningErrors: [], + onPremisesSecurityIdentifier: null, + onPremisesSyncEnabled: null, + preferredDataLocation: null, + proxyAddresses: [ + 'SMTP:finance@contoso.onmicrosoft.com' + ], + renewedDateTime: '2017-11-29T03:27:05Z', + securityEnabled: false, + visibility: 'Public' + }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) { + if (opts.data === `https://contoso.sharepoint.com`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.20530.12001", + "ErrorInfo": null, + "TraceCorrelationId": "10f1829f-d000-0000-5962-1110d33e2cf2" + }, + 4, + { + "IsNull": false + }, + 5, + { + "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.SiteProperties", + "_ObjectIdentity_": "10f1829f-d000-0000-5962-1110d33e2cf2|908bed80-a04a-4433-b4a0-883d9847d110:095efa67-57fa-40c7-b7cc-e96dc3e5780c\nSiteProperties\nhttps%3a%2f%2fcontoso.sharepoint.com%2fsites%2fdemosite", + "GroupId": "/Guid(58587cc9-560c-4adb-a849-e669bd37c5f8)/" + } + ]); + } + } + + throw 'invalid request'; + }); + + await spo.removeSite('https://contoso.sharepoint.com', false, true, logger, true); + assert(postStub.called); + }); + + it('handles exception when group still exists', async () => { + sinon.stub(spo, 'getSpoAdminUrl').resolves('https://contoso-admin.sharepoint.com'); + sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' }); + sinon.stub(aadGroup, 'getGroupById').rejects(new Error(`Can't find the group`)); + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/deletedItems/Microsoft.Graph.Group?$select=id&$filter=groupTypes/any(c:c+eq+'Unified') and startswith(id, '58587cc9-560c-4adb-a849-e669bd37c5f8')`) { + return { + value: [{ + "id": "58587cc9-560c-4adb-a849-e669bd37c5f8" + }] + }; + } + + throw 'invalid request'; + }); + + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) { + if (opts.data === `https://contoso.sharepoint.com`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.20530.12001", + "ErrorInfo": null, + "TraceCorrelationId": "10f1829f-d000-0000-5962-1110d33e2cf2" + }, + 4, + { + "IsNull": false + }, + 5, + { + "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.SiteProperties", + "_ObjectIdentity_": "10f1829f-d000-0000-5962-1110d33e2cf2|908bed80-a04a-4433-b4a0-883d9847d110:095efa67-57fa-40c7-b7cc-e96dc3e5780c\nSiteProperties\nhttps%3a%2f%2fcontoso.sharepoint.com%2fsites%2fdemosite", + "GroupId": "/Guid(58587cc9-560c-4adb-a849-e669bd37c5f8)/" + } + ]); + } + } + + throw 'invalid request'; + }); + + try { + await spo.removeSite('https://contoso.sharepoint.com', true, true, logger, true); + assert.fail('No error message thrown.'); + } + catch (ex) { + assert.deepStrictEqual(ex, `Site group still exists in the deleted groups. The site won't be removed.`); + } + }); + + it('removes a site succesfully after it still exists in the Microsoft 365 deleted groups.', async () => { + sinon.stub(spo, 'getSpoAdminUrl').resolves('https://contoso-admin.sharepoint.com'); + sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' }); + sinon.stub(aadGroup, 'getGroupById').rejects(new Error(`Can't find the group`)); + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/deletedItems/Microsoft.Graph.Group?$select=id&$filter=groupTypes/any(c:c+eq+'Unified') and startswith(id, '58587cc9-560c-4adb-a849-e669bd37c5f8')`) { + return { + value: [] + }; + } + + throw 'invalid request'; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) { + if (opts.data === `https://contoso.sharepoint.com`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.20530.12001", + "ErrorInfo": null, + "TraceCorrelationId": "10f1829f-d000-0000-5962-1110d33e2cf2" + }, + 4, + { + "IsNull": false + }, + 5, + { + "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.SiteProperties", + "_ObjectIdentity_": "10f1829f-d000-0000-5962-1110d33e2cf2|908bed80-a04a-4433-b4a0-883d9847d110:095efa67-57fa-40c7-b7cc-e96dc3e5780c\nSiteProperties\nhttps%3a%2f%2fcontoso.sharepoint.com%2fsites%2fdemosite", + "GroupId": "/Guid(58587cc9-560c-4adb-a849-e669bd37c5f8)/" + } + ]); + } + } + + if (opts.url === "https://contoso-admin.sharepoint.com/_api/GroupSiteManager/Delete?siteUrl='https://contoso.sharepoint.com'") { + return { + "data": { + "odata.null": true + } + }; + } + + throw 'invalid request'; + }); + + await spo.removeSite('https://contoso.sharepoint.com', true, true, logger, true); + assert(postStub.called); + }); }); \ No newline at end of file diff --git a/src/utils/spo.ts b/src/utils/spo.ts index 9136294b351..62306308686 100644 --- a/src/utils/spo.ts +++ b/src/utils/spo.ts @@ -17,6 +17,7 @@ import { SiteProperties } from '../m365/spo/commands/site/SiteProperties.js'; import { aadGroup } from './aadGroup.js'; import { SharingCapabilities } from '../m365/spo/commands/site/SharingCapabilities.js'; import { WebProperties } from '../m365/spo/commands/web/WebProperties.js'; +import chalk from 'chalk'; export interface ContextInfo { FormDigestTimeoutSeconds: number; @@ -1453,5 +1454,185 @@ export const spo = { const webProperties: WebProperties = await request.get(requestOptions); return webProperties; + }, + + async getSite(url: string, logger: Logger, verbose: boolean): Promise { + if (verbose) { + logger.logToStderr(`Retrieving the site properties for ${url}`); + } + + const requestOptions: any = { + url: `${url}/_api/site`, + headers: { + accept: 'application/json;odata=nometadata' + }, + responseType: 'json' + }; + + const res = await request.get(requestOptions); + + return res; + }, + + /** + * Removes a site and removes it from the recycle bin if skipRecycleBin is true + * @param url The url of the site + * @param skipRecycleBin Set to remove the site from the recycle bin + * @param wait set to wait until finished + * @param logger the Logger object + * @param verbose set if verbose logging should be logged + */ + async removeSite(url: string, skipRecycleBin: boolean, wait: boolean, logger: Logger, verbose: boolean): Promise { + const spoAdminUrl: string = await spo.getSpoAdminUrl(logger, verbose); + let context: FormDigestInfo = await spo.ensureFormDigest(spoAdminUrl, logger, undefined, verbose); + + if (verbose) { + logger.logToStderr(`Retrieving the group Id of the site ${url}`); + } + + const requestOptions: any = { + url: `${spoAdminUrl as string}/_vti_bin/client.svc/ProcessQuery`, + headers: { + 'X-RequestDigest': context.FormDigestValue + }, + data: `${formatting.escapeXml(url)}` + }; + + const response: string = await request.post(requestOptions); + const json: ClientSvcResponse = JSON.parse(response); + const responseContent: ClientSvcResponseContents = json[0]; + + if (responseContent.ErrorInfo) { + throw responseContent.ErrorInfo.ErrorMessage; + } + + const groupId: string = json[json.length - 1].GroupId.replace('/Guid(', '').replace(')/', ''); + + if (groupId === '00000000-0000-0000-0000-000000000000') { + if (verbose) { + logger.logToStderr('Site is not groupified. Going ahead with the conventional site deletion options'); + } + + context = await spo.ensureFormDigest(spoAdminUrl, logger, context, verbose); + + await spo.deleteSite(spoAdminUrl, url, wait, logger, verbose); + + if (skipRecycleBin) { + if (verbose) { + logger.logToStderr(`Also deleting site from tenant recycle bin ${url}...`); + } + + await this.deleteSiteFromTheRecycleBin(url, logger, verbose, wait); + } + } + else { + if (verbose) { + logger.logToStderr(`Site attached to group ${groupId}. Initiating group delete operation via Graph API`); + } + + try { + const group = await aadGroup.getGroupById(groupId); + if (skipRecycleBin || wait) { + logger.logToStderr(chalk.yellow(`Entered site is a groupified site. Hence, the parameters 'skipRecycleBin' and 'wait' will not be applicable.`)); + } + + await aadGroup.removeGroup(group.id!, logger, verbose); + await spo.deleteSite(spoAdminUrl, url, wait, logger, verbose); + } + catch (err: any) { + if (verbose) { + logger.logToStderr(`Site group doesn't exist. Searching in the Microsoft 365 deleted groups.`); + } + + const requestOptions: any = { + url: `https://graph.microsoft.com/v1.0/directory/deletedItems/Microsoft.Graph.Group?$select=id&$filter=groupTypes/any(c:c+eq+'Unified') and startswith(id, '${groupId}')`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + const deletedGroups = await request.get<{ value: { id: string }[] }>(requestOptions); + + if (deletedGroups.value.length === 0) { + if (verbose) { + logger.logToStderr("Site group doesn't exist anymore. Deleting the site."); + } + + if (wait) { + logger.logToStderr(chalk.yellow(`Entered site is a groupified site. Hence, the parameter 'wait' will not be applicable.`)); + } + + const requestOptions: any = { + url: `${spoAdminUrl}/_api/GroupSiteManager/Delete?siteUrl='${url}'`, + headers: { + 'content-type': 'application/json;odata=nometadata', + accept: 'application/json;odata=nometadata' + }, + responseType: 'json' + }; + + await request.post(requestOptions); + } + else { + throw `Site group still exists in the deleted groups. The site won't be removed.`; + } + } + } + }, + + /** + * Deletes a site + * @param spoAdminUrl The url of the SharePoint Admin centre + * @param url The url of the site + * @param wait set to wait until finished + * @param logger the Logger object + * @param verbose set if verbose logging should be logged + */ + async deleteSite(spoAdminUrl: string, url: string, wait: boolean, logger: Logger, verbose: boolean, givenContext?: FormDigestInfo): Promise { + const context: FormDigestInfo = await spo.ensureFormDigest(spoAdminUrl as string, logger, givenContext, verbose); + + if (verbose) { + logger.logToStderr(`Deleting site ${url}...`); + } + + const requestOptions: any = { + url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`, + headers: { + 'X-RequestDigest': context.FormDigestValue + }, + data: `${formatting.escapeXml(url)}` + }; + + const resonse: string = await request.post(requestOptions); + + const json: ClientSvcResponse = JSON.parse(resonse); + const responseClient: ClientSvcResponseContents = json[0]; + if (responseClient.ErrorInfo) { + throw responseClient.ErrorInfo.ErrorMessage; + } + else { + const operation: SpoOperation = json[json.length - 1]; + const isComplete: boolean = operation.IsComplete; + + if (!wait || isComplete) { + return; + } + + await new Promise((resolve: () => void, reject: (error: any) => void): void => { + setTimeout(() => { + spo.waitUntilFinished({ + operationId: JSON.stringify(operation._ObjectIdentity_), + siteUrl: spoAdminUrl as string, + resolve, + reject, + logger, + currentContext: context as FormDigestInfo, + debug: verbose, + verbose: verbose + }); + }, operation.PollingInterval); + }); + } } }; \ No newline at end of file