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
192 changes: 102 additions & 90 deletions src/m365/entra/commands/m365group/m365group-add.spec.ts

Large diffs are not rendered by default.

175 changes: 60 additions & 115 deletions src/m365/entra/commands/m365group/m365group-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,46 @@ import { Group, User } from '@microsoft/microsoft-graph-types';
import { setTimeout } from 'timers/promises';
import fs from 'fs';
import path from 'path';
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 { zod } from '../../../../utils/zod.js';
import GraphCommand from '../../../base/GraphCommand.js';
import commands from '../../commands.js';

interface CommandArgs {
options: Options;
enum GroupVisibility {
Private = 'Private',
Public = 'Public',
HiddenMembership = 'HiddenMembership'
}

interface Options extends GlobalOptions {
displayName: string;
mailNickname: string;
description?: string;
owners?: string;
members?: string;
visibility?: string;
logoPath?: string;
allowMembersToPost?: boolean;
hideGroupInOutlook?: boolean;
subscribeNewGroupMembers?: boolean;
welcomeEmailDisabled?: boolean;
const options = globalOptionsZod
.extend({
displayName: zod.alias('n', z.string()),
mailNickname: zod.alias('m', z.string()),
description: zod.alias('d', z.string().optional()),
owners: z.string().optional(),
members: z.string().optional(),
visibility: zod.coercedEnum(GroupVisibility).optional(),
logoPath: zod.alias('l', z.string().optional()),
allowMembersToPost: z.boolean().optional(),
hideGroupInOutlook: z.boolean().optional(),
subscribeNewGroupMembers: z.boolean().optional(),
welcomeEmailDisabled: z.boolean().optional()
})
.strict();

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

interface CommandArgs {
options: Options;
}

class EntraM365GroupAddCommand extends GraphCommand {
private static numRepeat: number = 15;
private pollingInterval: number = 500;
private allowedVisibilities: string[] = ['Private', 'Public', 'HiddenMembership'];

public get name(): string {
return commands.M365GROUP_ADD;
Expand All @@ -40,123 +51,57 @@ class EntraM365GroupAddCommand extends GraphCommand {
return 'Creates a Microsoft 365 Group';
}

constructor() {
super();

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

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
description: typeof args.options.description !== 'undefined',
owners: typeof args.options.owners !== 'undefined',
members: typeof args.options.members !== 'undefined',
logoPath: typeof args.options.logoPath !== 'undefined',
visibility: typeof args.options.visibility !== 'undefined',
allowMembersToPost: !!args.options.allowMembersToPost,
hideGroupInOutlook: !!args.options.hideGroupInOutlook,
subscribeNewGroupMembers: !!args.options.subscribeNewGroupMembers,
welcomeEmailDisabled: !!args.options.welcomeEmailDisabled
});
});
}

#initOptions(): void {
this.options.unshift(
{
option: '-n, --displayName <displayName>'
},
{
option: '-m, --mailNickname <mailNickname>'
},
{
option: '-d, --description [description]'
},
{
option: '--owners [owners]'
},
{
option: '--members [members]'
},
{
option: '--visibility [visibility]',
autocomplete: this.allowedVisibilities
},
{
option: '--allowMembersToPost [allowMembersToPost]',
autocomplete: ['true', 'false']
},
{
option: '--hideGroupInOutlook [hideGroupInOutlook]',
autocomplete: ['true', 'false']
},
{
option: '--subscribeNewGroupMembers [subscribeNewGroupMembers]',
autocomplete: ['true', 'false']
},
{
option: '--welcomeEmailDisabled [welcomeEmailDisabled]',
autocomplete: ['true', 'false']
},
{
option: '-l, --logoPath [logoPath]'
}
);
}

#initTypes(): void {
this.types.string.push('displayName', 'mailNickname', 'description', 'owners', 'members', 'visibility', 'logoPath');
this.types.boolean.push('allowMembersToPost', 'hideGroupInOutlook', 'subscribeNewGroupMembers', 'welcomeEmailDisabled');
public get schema(): z.ZodTypeAny | undefined {
return options;
}

#initValidators(): void {
this.validators.push(
async (args: CommandArgs) => {
if (args.options.owners) {
const owners: string[] = args.options.owners.split(',').map(o => o.trim());
public getRefinedSchema(schema: typeof options): z.ZodEffects<any> | undefined {
return schema
.refine(options => {
if (options.owners) {
const owners: string[] = options.owners.split(',').map(o => o.trim());
for (let i = 0; i < owners.length; i++) {
if (owners[i].indexOf('@') < 0) {
return `${owners[i]} is not a valid userPrincipalName`;
return false;
}
}
}

if (args.options.members) {
const members: string[] = args.options.members.split(',').map(m => m.trim());
return true;
}, {
message: 'Invalid userPrincipalName for owners'
})
.refine(options => {
if (options.members) {
const members: string[] = options.members.split(',').map(m => m.trim());
for (let i = 0; i < members.length; i++) {
if (members[i].indexOf('@') < 0) {
return `${members[i]} is not a valid userPrincipalName`;
return false;
}
}
}

if (args.options.mailNickname.indexOf(' ') !== -1) {
return 'The option mailNickname cannot contain spaces.';
}

if (args.options.logoPath) {
const fullPath: string = path.resolve(args.options.logoPath);

return true;
}, {
message: 'Invalid userPrincipalName for members'
})
.refine(options => options.mailNickname.indexOf(' ') === -1, {
message: 'The option mailNickname cannot contain spaces.'
})
.refine(options => {
if (options.logoPath) {
const fullPath: string = path.resolve(options.logoPath);

if (!fs.existsSync(fullPath)) {
return `File '${fullPath}' not found`;
return false;
}

if (fs.lstatSync(fullPath).isDirectory()) {
return `Path '${fullPath}' points to a directory`;
return false;
}
}

if (args.options.visibility && this.allowedVisibilities.map(x => x.toLowerCase()).indexOf(args.options.visibility.toLowerCase()) === -1) {
return `${args.options.visibility} is not a valid visibility. Allowed values are ${this.allowedVisibilities.join(', ')}`;
}

return true;
}
);
}, {
message: 'Invalid logoPath'
});
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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';
Expand All @@ -12,13 +14,13 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './m365group-conversation-list.js';
import { entraGroup } from '../../../../utils/entraGroup.js';
import { cli } from '../../../../cli/cli.js';

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

const jsonOutput = {
"value": [
Expand Down Expand Up @@ -53,6 +55,7 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => {
sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves('00000000-0000-0000-0000-000000000000');
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
});

beforeEach(() => {
Expand Down Expand Up @@ -94,13 +97,15 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => {
it('defines correct properties for the default output', () => {
assert.deepStrictEqual(command.defaultProperties(), ['topic', 'lastDeliveredDateTime', 'id']);
});

it('fails validation if the groupId is not a valid GUID', async () => {
const actual = await command.validate({ options: { groupId: 'not-c49b-4fd4-8223-28f0ac3a6402' } }, commandInfo);
assert.notStrictEqual(actual, true);
const actual = commandOptionsSchema.safeParse({ groupId: 'not-c49b-4fd4-8223-28f0ac3a6402' });
assert.strictEqual(actual.success, false);
});

it('passes validation if the groupId is a valid GUID', async () => {
const actual = await command.validate({ options: { groupId: '1caf7dcd-7e83-4c3a-94f7-932a1299c844' } }, commandInfo);
assert.strictEqual(actual, true);
const actual = commandOptionsSchema.safeParse({ groupId: '1caf7dcd-7e83-4c3a-94f7-932a1299c844' });
assert.strictEqual(actual.success, true);
});

it('Retrieve conversations for the group specified by groupId in the tenant (verbose)', async () => {
Expand All @@ -112,9 +117,9 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => {
});

await command.action(logger, {
options: {
options: commandOptionsSchema.parse({
verbose: true, groupId: "00000000-0000-0000-0000-000000000000"
}
})
});
assert(loggerLogSpy.calledWith(
jsonOutput.value
Expand All @@ -130,9 +135,9 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => {
});

await command.action(logger, {
options: {
options: commandOptionsSchema.parse({
verbose: true, groupName: "Finance"
}
})
});
assert(loggerLogSpy.calledWith(
jsonOutput.value
Expand All @@ -142,7 +147,7 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => {
it('correctly handles error when listing conversations', async () => {
sinon.stub(request, 'get').rejects(new Error('An error has occurred'));

await assert.rejects(command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000" } } as any),
await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ groupId: "00000000-0000-0000-0000-000000000000" }) } as any),
new CommandError('An error has occurred'));
});

Expand All @@ -152,7 +157,7 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => {
sinonUtil.restore(entraGroup.isUnifiedGroup);
sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false);

await assert.rejects(command.action(logger, { options: { groupId: groupId } } as any),
await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ groupId: groupId }) } as any),
new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`));
});

Expand Down
62 changes: 19 additions & 43 deletions src/m365/entra/commands/m365group/m365group-conversation-list.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { Conversation } from '@microsoft/microsoft-graph-types';
import { z } from 'zod';
import { Logger } from '../../../../cli/Logger.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import { globalOptionsZod } from '../../../../Command.js';
import { odata } from '../../../../utils/odata.js';
import { validation } from '../../../../utils/validation.js';
import { zod } from '../../../../utils/zod.js';
import GraphCommand from '../../../base/GraphCommand.js';
import commands from '../../commands.js';
import { entraGroup } from '../../../../utils/entraGroup.js';

const options = globalOptionsZod
.extend({
groupId: zod.alias('i', z.string().uuid().optional()),
groupName: zod.alias('n', z.string().optional())
})
.strict();

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

interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
groupId?: string;
groupName?: string;
}

class EntraM365GroupConversationListCommand extends GraphCommand {
public get name(): string {
return commands.M365GROUP_CONVERSATION_LIST;
Expand All @@ -29,44 +34,15 @@ class EntraM365GroupConversationListCommand extends GraphCommand {
return ['topic', 'lastDeliveredDateTime', 'id'];
}

constructor() {
super();

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

#initOptions(): void {
this.options.unshift(
{
option: '-i, --groupId [groupId]'
},
{
option: '-n, --groupName [groupName]'
}
);
}

#initValidators(): void {
this.validators.push(
async (args: CommandArgs) => {
if (args.options.groupId && !validation.isValidGuid(args.options.groupId as string)) {
return `${args.options.groupId} is not a valid GUID`;
}

return true;
}
);
}

#initOptionSets(): void {
this.optionSets.push({ options: ['groupId', 'groupName'] });
public get schema(): z.ZodTypeAny | undefined {
return options;
}

#initTypes(): void {
this.types.string.push('groupId', 'groupName');
public getRefinedSchema(schema: typeof options): z.ZodEffects<any> | undefined {
return schema
.refine(options => [options.groupId, options.groupName].filter(Boolean).length === 1, {
message: 'Specify either groupId or groupName'
});
}

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