Skip to content

Commit

Permalink
New command: spo page publish. Closes #6420
Browse files Browse the repository at this point in the history
  • Loading branch information
Saurabh7019 committed Oct 21, 2024
1 parent 1a88ed2 commit 40c4aaf
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 0 deletions.
41 changes: 41 additions & 0 deletions docs/docs/cmd/spo/page/page-publish.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Global from '/docs/cmd/_global.mdx';

# spo page publish

Publishes a modern page

## Usage

```sh
m365 spo page publish [options]
```

## Options

```md definition-list
`-u, --webUrl <webUrl>`
: URL of the site where the page is located.

`-n, --name <name>`
: Name of the page.
```

<Global />

## Examples

Publish a modern page

```sh
m365 spo page publish --webUrl https://contoso.sharepoint.com/sites/Marketing --name "Style guide.aspx"
```

Publish a modern page in a subfolder

```sh
m365 spo page publish --webUrl https://contoso.sharepoint.com/sites/Marketing --name "/Styles/Guide.aspx"
```

## Response

The command won't return a response on success.
5 changes: 5 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3157,6 +3157,11 @@ const sidebars: SidebarsConfig = {
label: 'page list',
id: 'cmd/spo/page/page-list'
},
{
type: 'doc',
label: 'page publish',
id: 'cmd/spo/page/page-publish'
},
{
type: 'doc',
label: 'page remove',
Expand Down
1 change: 1 addition & 0 deletions src/m365/spo/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export default {
PAGE_COPY: `${prefix} page copy`,
PAGE_GET: `${prefix} page get`,
PAGE_LIST: `${prefix} page list`,
PAGE_PUBLISH: `${prefix} page publish`,
PAGE_REMOVE: `${prefix} page remove`,
PAGE_SET: `${prefix} page set`,
PAGE_CLIENTSIDEWEBPART_ADD: `${prefix} page clientsidewebpart add`,
Expand Down
178 changes: 178 additions & 0 deletions src/m365/spo/commands/page/page-publish.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import assert from 'assert';
import sinon from 'sinon';
import { z } from 'zod';
import auth from '../../../../Auth.js';
import { cli } from '../../../../cli/cli.js';
import { CommandInfo } from '../../../../cli/CommandInfo.js';
import { Logger } from '../../../../cli/Logger.js';
import { CommandError } from '../../../../Command.js';
import request from '../../../../request.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './page-publish.js';
import { urlUtil } from '../../../../utils/urlUtil.js';
import { formatting } from '../../../../utils/formatting.js';

describe(commands.PAGE_PUBLISH, () => {
let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
let postStub: sinon.SinonStub;
let commandOptionsSchema: z.ZodTypeAny;

const webUrl = 'https://contoso.sharepoint.com/sites/Marketing';
const serverRelativeUrl = urlUtil.getServerRelativeSiteUrl(webUrl);
const pageName = 'HR.aspx';

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').returns();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
});

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

const serverRelativePageUrl = `${serverRelativeUrl}/SitePages/${pageName}`;
postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `${webUrl}/_api/web/GetFileByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter(serverRelativePageUrl)}')/Publish()`) {
return;
}

throw 'Invalid request: ' + opts.url;
});
});

afterEach(() => {
sinonUtil.restore([
request.post
]);
});

after(() => {
sinon.restore();
auth.connection.active = false;
});

it('has correct name', () => {
assert.strictEqual(command.name, commands.PAGE_PUBLISH);
});

it('has a description', () => {
assert.notStrictEqual(command.description, null);
});

it('logs no command output', async () => {
await command.action(logger,
{
options: {
webUrl: webUrl,
name: pageName
}
});

assert(loggerLogSpy.notCalled);
});

it('correctly publishes page', async () => {
await command.action(logger,
{
options: {
webUrl: webUrl,
name: pageName
}
});

assert(postStub.calledOnce);
});

it('correctly publishes a page when extension is not specified', async () => {
await command.action(logger,
{
options: {
webUrl: webUrl,
name: pageName.substring(0, pageName.lastIndexOf('.')),
verbose: true
}
});

assert(postStub.calledOnce);
});

it('correctly publishes a nested page', async () => {
const pageUrl = '/folder1/folder2/' + pageName;
postStub.restore();

postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `${webUrl}/_api/web/GetFileByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter(serverRelativeUrl + '/SitePages' + pageUrl)}')/Publish()`) {
return;
}

throw 'Invalid request: ' + opts.url;
});

await command.action(logger,
{
options: {
webUrl: webUrl,
name: pageUrl
}
});

assert(postStub.calledOnce);
});

it('correctly handles API error', async () => {
postStub.restore();
const errorMessage = 'The file /sites/Marketing/SitePages/My-new-page.aspx does not exist.';

sinon.stub(request, 'post').rejects({
error: {
'odata.error': {
message: {
lang: 'en-US',
value: errorMessage
}
}
}
});

await assert.rejects(command.action(logger, { options: { webUrl: webUrl, name: pageName } }),
new CommandError(errorMessage));
});

it('fails validation if webUrl is not a valid SharePoint URL', async () => {
const actual = commandOptionsSchema.safeParse({ webUrl: 'foo' });
assert.strictEqual(actual.success, false);
});

it('passes validation when the webUrl is a valid SharePoint URL and name is specified', async () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, name: pageName });
assert.strictEqual(actual.success, true);
});

it('passes validation when name has no extension', async () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, name: 'page' });
assert.strictEqual(actual.success, true);
});
});
69 changes: 69 additions & 0 deletions src/m365/spo/commands/page/page-publish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { z } from 'zod';
import { zod } from '../../../../utils/zod.js';
import { Logger } from '../../../../cli/Logger.js';
import { globalOptionsZod } from '../../../../Command.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { formatting } from '../../../../utils/formatting.js';
import { urlUtil } from '../../../../utils/urlUtil.js';
import { validation } from '../../../../utils/validation.js';
import SpoCommand from '../../../base/SpoCommand.js';
import commands from '../../commands.js';

const options = globalOptionsZod
.extend({
webUrl: zod.alias('u', z.string()
.refine(url => validation.isValidSharePointUrl(url) === true, url => ({
message: `'${url}' is not a valid SharePoint Online site URL.`
}))
),
name: zod.alias('n', z.string())
})
.strict();
declare type Options = z.infer<typeof options>;

interface CommandArgs {
options: Options;
}

class SpoPagePublishCommand extends SpoCommand {
public get name(): string {
return commands.PAGE_PUBLISH;
}

public get description(): string {
return 'Publishes a modern page';
}

public get schema(): z.ZodTypeAny {
return options;
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
try {
// Remove leading slashes from the page name (page can be nested in folders)
let pageName: string = urlUtil.removeLeadingSlashes(args.options.name);
if (!pageName.toLowerCase().endsWith('.aspx')) {
pageName += '.aspx';
}

if (this.verbose) {
await logger.logToStderr(`Publishing page ${pageName}...`);
}

const filePath = `${urlUtil.getServerRelativeSiteUrl(args.options.webUrl)}/SitePages/${pageName}`;
const requestOptions: CliRequestOptions = {
url: `${args.options.webUrl}/_api/web/GetFileByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter(filePath)}')/Publish()`,
headers: {
accept: 'application/json;odata=nometadata'
}
};

await request.post(requestOptions);
}
catch (err: any) {
this.handleRejectedODataJsonPromise(err);
}
}
}

export default new SpoPagePublishCommand();

0 comments on commit 40c4aaf

Please sign in to comment.