-
Notifications
You must be signed in to change notification settings - Fork 328
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New command: spo page publish. Closes #6420
- Loading branch information
1 parent
1a88ed2
commit 40c4aaf
Showing
5 changed files
with
294 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |