Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 39 additions & 29 deletions src/m365/spo/commands/folder/folder-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ 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 './folder-add.js';
import command, { options } from './folder-add.js';
import { FolderColorValues } from './FolderColor.js';

describe(commands.FOLDER_ADD, () => {
let log: any[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;
let stubPostResponses: any;

const addResponse = { "Exists": true, "IsWOPIEnabled": false, "ItemCount": 0, "Name": "abc", "ProgID": null, "ServerRelativeUrl": "/sites/test1/Shared Documents/abc", "TimeCreated": "2018-05-02T23:21:45Z", "TimeLastModified": "2018-05-02T23:21:45Z", "UniqueId": "0ac3da45-cacf-4c31-9b38-9ef3697d5a66", "WelcomePage": "" };
Expand Down Expand Up @@ -52,6 +54,7 @@ describe(commands.FOLDER_ADD, () => {
});
};
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
});

beforeEach(() => {
Expand Down Expand Up @@ -104,24 +107,24 @@ describe(commands.FOLDER_ADD, () => {
stubPostResponses(error);

await assert.rejects(command.action(logger, {
options: {
options: commandOptionsSchema.parse({
webUrl: 'https://contoso.sharepoint.com',
parentFolderUrl: '/Shared Documents',
name: 'abc'
}
} as any), new CommandError('An error has occurred'));
})
}), new CommandError('An error has occurred'));
});

it('should correctly handle folder add success request', async () => {
stubPostResponses();

await command.action(logger, {
options: {
options: commandOptionsSchema.parse({
debug: true,
webUrl: 'https://contoso.sharepoint.com',
parentFolderUrl: '/Shared Documents',
name: 'abc'
}
})
});
assert(loggerLogSpy.lastCall.calledWith({ "Exists": true, "IsWOPIEnabled": false, "ItemCount": 0, "Name": "abc", "ProgID": null, "ServerRelativeUrl": "/sites/test1/Shared Documents/abc", "TimeCreated": "2018-05-02T23:21:45Z", "TimeLastModified": "2018-05-02T23:21:45Z", "UniqueId": "0ac3da45-cacf-4c31-9b38-9ef3697d5a66", "WelcomePage": "" }));
});
Expand All @@ -130,12 +133,12 @@ describe(commands.FOLDER_ADD, () => {
const request: sinon.SinonStub = stubPostResponses();

await command.action(logger, {
options: {
options: commandOptionsSchema.parse({
debug: true,
webUrl: 'https://contoso.sharepoint.com',
parentFolderUrl: '/Shared Documents',
name: 'abc'
}
})
});
assert(request.calledWith({
url: `https://contoso.sharepoint.com/_api/web/folders/addUsingPath(decodedUrl='${formatting.encodeQueryParameter('/Shared Documents/abc')}')`,
Expand All @@ -149,12 +152,12 @@ describe(commands.FOLDER_ADD, () => {
const request: sinon.SinonStub = stubPostResponses();

await command.action(logger, {
options: {
options: commandOptionsSchema.parse({
debug: true,
webUrl: 'https://contoso.sharepoint.com/sites/test1',
parentFolderUrl: 'Shared Documents',
name: 'abc'
}
})
});
assert(request.calledWith({
url: `https://contoso.sharepoint.com/sites/test1/_api/web/folders/addUsingPath(decodedUrl='${formatting.encodeQueryParameter('/sites/test1/Shared Documents/abc')}')`,
Expand All @@ -172,7 +175,7 @@ describe(commands.FOLDER_ADD, () => {
throw 'Invalid request';
});

await command.action(logger, { options: { webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, color: colorNumber } });
await command.action(logger, { options: commandOptionsSchema.parse({ webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, color: colorNumber }) });
assert.deepStrictEqual(postStub.lastCall.args[0].data, {
coloringInformation: {
ColorHex: `${colorNumber}`
Expand All @@ -188,7 +191,7 @@ describe(commands.FOLDER_ADD, () => {
throw 'Invalid request';
});

await command.action(logger, { options: { webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, color: colorName } });
await command.action(logger, { options: commandOptionsSchema.parse({ webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, color: colorName }) });
assert.deepStrictEqual(postStub.lastCall.args[0].data, {
coloringInformation: {
ColorHex: colorNumber
Expand Down Expand Up @@ -220,42 +223,49 @@ describe(commands.FOLDER_ADD, () => {
});

await command.action(logger, {
options: {
options: commandOptionsSchema.parse({
debug: true,
webUrl: webUrl,
parentFolderUrl: '/Shared Documents/abc',
name: 'folder1',
ensureParentFolders: true
}
})
});

assert.strictEqual(requestGet.lastCall.args[0].url, `https://contoso.sharepoint.com/_api/web/GetFolderByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter('Shared Documents/abc')}')?$select=Exists`);

assert.strictEqual(requestPost.callCount, 2);
});

it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => {
const actual = await command.validate({ options: { webUrl: 'foo', parentFolderUrl: parentFolder, name: folderName } }, commandInfo);
assert.notStrictEqual(actual, true);
it('fails validation if the webUrl option is not a valid SharePoint site URL', () => {
const actual = commandOptionsSchema.safeParse({ webUrl: 'foo', parentFolderUrl: parentFolder, name: folderName });
assert.strictEqual(actual.success, false);
assert.strictEqual(actual.error?.issues[0].message, `'foo' is not a valid SharePoint Online site URL.`);
});

it('passes validation if the webUrl option is a valid SharePoint site URL and parentFolderUrl specified', () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName });
assert.strictEqual(actual.success, true);
});

it('passes validation if the webUrl option is a valid SharePoint site URL and parentFolderUrl specified', async () => {
const actual = await command.validate({ options: { webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName } }, commandInfo);
assert.strictEqual(actual, true);
it('fails validation if color is passed as string and color is not a valid color', () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, color: 'invalid' });
assert.strictEqual(actual.success, false);
assert.strictEqual(actual.error?.issues[0].message, `'invalid' is not a valid value for option 'color'. Allowed values are ${Object.keys(FolderColorValues).join(', ')}, ${Object.values(FolderColorValues).join(', ')}.`);
});

it('fails validation if color is passed as string and color is not a valid color', async () => {
const actual = await command.validate({ options: { webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, color: 'invalid' } }, commandInfo);
assert.notStrictEqual(actual, true);
it('passes validation if color is passed as string and color is a valid color', () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, color: colorName });
assert.strictEqual(actual.success, true);
});

it('passes validation if color is passed as string and color is a valid color', async () => {
const actual = await command.validate({ options: { webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, color: colorName } }, commandInfo);
assert.strictEqual(actual, true);
it('passes validation if color is passed as number and color is a valid color', () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, color: colorNumber });
assert.strictEqual(actual.success, true);
});

it('passes validation if color is passed as number and color is a valid color', async () => {
const actual = await command.validate({ options: { webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, color: colorNumber } }, commandInfo);
assert.strictEqual(actual, true);
it('fails validation with unknown options', () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, parentFolderUrl: parentFolder, name: folderName, unknownOption: 'value' });
assert.strictEqual(actual.success, false);
});
});
90 changes: 23 additions & 67 deletions src/m365/spo/commands/folder/folder-add.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod';
import { Logger } from '../../../../cli/Logger.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import { globalOptionsZod } from '../../../../Command.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { formatting } from '../../../../utils/formatting.js';
import { urlUtil } from '../../../../utils/urlUtil.js';
Expand All @@ -9,18 +10,29 @@ import commands from '../../commands.js';
import { FolderColorValues } from './FolderColor.js';
import { FolderProperties } from './FolderProperties.js';

const folderColorValues = [...Object.keys(FolderColorValues), ...Object.values(FolderColorValues)] as [string, ...string[]];

export const options = z.strictObject({
...globalOptionsZod.shape,
webUrl: z.string()
.refine(url => validation.isValidSharePointUrl(url) === true, {
error: e => `'${e.input}' is not a valid SharePoint Online site URL.`
})
.alias('u'),
parentFolderUrl: z.string().alias('p'),
name: z.string().alias('n'),
color: z.enum(folderColorValues, {
error: e => `'${e.input}' is not a valid value for option 'color'. Allowed values are ${Object.keys(FolderColorValues).join(', ')}, ${Object.values(FolderColorValues).join(', ')}.`
}).optional(),
ensureParentFolders: z.boolean().optional()
});

declare type Options = z.infer<typeof options>;

interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
webUrl: string;
parentFolderUrl: string;
name: string;
color?: string;
ensureParentFolders?: boolean
}

class SpoFolderAddCommand extends SpoCommand {
public get name(): string {
return commands.FOLDER_ADD;
Expand All @@ -30,64 +42,8 @@ class SpoFolderAddCommand extends SpoCommand {
return 'Creates a folder within a parent folder';
}

constructor() {
super();

this.#initTelemetry();
this.#initOptions();
this.#initValidators();
this.#initTypes();
}

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
color: typeof args.options.color !== 'undefined',
ensureParentFolders: !!args.options.ensureParentFolders
});
});
}

#initOptions(): void {
this.options.unshift(
{
option: '-u, --webUrl <webUrl>'
},
{
option: '-p, --parentFolderUrl <parentFolderUrl>'
},
{
option: '-n, --name <name>'
},
{
option: '--color [color]',
autocomplete: Object.keys(FolderColorValues)
},
{
option: '--ensureParentFolders'
}
);
}

#initValidators(): void {
this.validators.push(
async (args: CommandArgs) => {
const isValidSharePointUrl = validation.isValidSharePointUrl(args.options.webUrl);
if (isValidSharePointUrl !== true) {
return isValidSharePointUrl;
}

if (args.options.color && !Object.entries(FolderColorValues).flat().includes(args.options.color)) {
return `'${args.options.color}' is not a valid value for option 'color'. Allowed values are ${Object.keys(FolderColorValues).join(', ')}, ${Object.values(FolderColorValues).join(', ')}.`;
}

return true;
});
}

#initTypes(): void {
this.types.string.push('webUrl', 'parentFolderUrl', 'name', 'color');
this.types.boolean.push('ensureParentFolders');
public get schema(): z.ZodType | undefined {
return options;
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
Expand Down
Loading
Loading