Skip to content
Draft
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
138 changes: 59 additions & 79 deletions src/m365/graph/commands/changelog/changelog-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import commands from '../../commands.js';
import { sinonUtil } from './../../../../utils/sinonUtil.js';
import command from './changelog-list.js';
import command, { options } from './changelog-list.js';

describe(commands.CHANGELOG_LIST, () => {
let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;
const validVersions = 'beta,v1.0';
const validChangeType = 'Addition';
const validServices = 'Groups,Security';
Expand Down Expand Up @@ -94,6 +95,7 @@ describe(commands.CHANGELOG_LIST, () => {
sinon.stub(session, 'getId').returns('');
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
});

beforeEach(() => {
Expand Down Expand Up @@ -135,104 +137,82 @@ describe(commands.CHANGELOG_LIST, () => {
assert.deepStrictEqual(command.defaultProperties(), ['category', 'title', 'description']);
});

it('fails validation if versions contains an invalid value.', async () => {
const actual = command.validate({
options: {
versions: 'invalid'
}
}, commandInfo);
assert.notStrictEqual(actual, true);
it('fails validation if versions contains an invalid value.', () => {
const actual = commandOptionsSchema.safeParse({
versions: 'invalid'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if changeType is an invalid value.', async () => {
const actual = command.validate({
options: {
changeType: 'invalid'
}
}, commandInfo);
assert.notStrictEqual(actual, true);
it('fails validation if changeType is an invalid value.', () => {
const actual = commandOptionsSchema.safeParse({
changeType: 'invalid'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if services contains an invalid value.', async () => {
const actual = command.validate({
options: {
services: 'invalid'
}
}, commandInfo);
assert.notStrictEqual(actual, true);
it('fails validation if services contains an invalid value.', () => {
const actual = commandOptionsSchema.safeParse({
services: 'invalid'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if startDate is invalid ISO date.', async () => {
const actual = command.validate({
options: {
startDate: 'invalid'
}
}, commandInfo);
assert.notStrictEqual(actual, true);
it('fails validation if startDate is invalid ISO date.', () => {
const actual = commandOptionsSchema.safeParse({
startDate: 'invalid'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if endDate is invalid ISO date.', async () => {
const actual = command.validate({
options: {
endDate: 'invalid'
}
}, commandInfo);
assert.notStrictEqual(actual, true);
it('fails validation if endDate is invalid ISO date.', () => {
const actual = commandOptionsSchema.safeParse({
endDate: 'invalid'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if endDate is earlier than startDate.', async () => {
const actual = command.validate({
options: {
endDate: '2018-11-01',
startDate: '2018-12-01'
}
}, commandInfo);
assert.notStrictEqual(actual, true);
it('fails validation if endDate is earlier than startDate.', () => {
const actual = commandOptionsSchema.safeParse({
endDate: '2018-11-01',
startDate: '2018-12-01'
});
assert.notStrictEqual(actual.success, true);
});

it('passes validation when valid versions specified', async () => {
const actual = await command.validate({
options: {
versions: validVersions
}
}, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when valid versions specified', () => {
const actual = commandOptionsSchema.safeParse({
versions: validVersions
});
assert.strictEqual(actual.success, true);
});

it('passes validation when valid changeType specified', async () => {
const actual = await command.validate({
options: {
changeType: validChangeType
}
}, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when valid changeType specified', () => {
const actual = commandOptionsSchema.safeParse({
changeType: validChangeType
});
assert.strictEqual(actual.success, true);
});

it('passes validation when valid services specified', async () => {
const actual = await command.validate({
options: {
services: validServices
}
}, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when valid services specified', () => {
const actual = commandOptionsSchema.safeParse({
services: validServices
});
assert.strictEqual(actual.success, true);
});

it('passes validation when valid startDate specified', async () => {
const actual = await command.validate({
options: {
startDate: validStartDate
}
}, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when valid startDate specified', () => {
const actual = commandOptionsSchema.safeParse({
startDate: validStartDate
});
assert.strictEqual(actual.success, true);
});

it('passes validation when valid endDate specified', async () => {
const actual = await command.validate({
options: {
endDate: validEndDate
}
}, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when valid endDate specified', () => {
const actual = commandOptionsSchema.safeParse({
endDate: validEndDate
});
assert.strictEqual(actual.success, true);
});

it('retrieves changelog list', async () => {
Expand Down
157 changes: 70 additions & 87 deletions src/m365/graph/commands/changelog/changelog-list.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
import { DOMParser } from '@xmldom/xmldom';
import GlobalOptions from '../../../../GlobalOptions.js';
import { z } from 'zod';
import { cli } from '../../../../cli/cli.js';
import { Logger } from '../../../../cli/Logger.js';
import { globalOptionsZod } from '../../../../Command.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { md } from '../../../../utils/md.js';
import { validation } from '../../../../utils/validation.js';
import AnonymousCommand from '../../../base/AnonymousCommand.js';
import { Changelog, ChangelogItem } from '../../Changelog.js';
import commands from '../../commands.js';

const allowedVersions = ['beta', 'v1.0'];
const allowedChangeTypes = ['Addition', 'Change', 'Deletion', 'Deprecation'];
const allowedServices = [
'Applications', 'Calendar', 'Change notifications', 'Cloud communications',
'Compliance', 'Cross-device experiences', 'Customer booking', 'Device and app management',
'Education', 'Files', 'Financials', 'Groups',
'Identity and access', 'Mail', 'Notes', 'Notifications',
'People and workplace intelligence', 'Personal contacts', 'Reports', 'Search',
'Security', 'Sites and lists', 'Tasks and plans', 'Teamwork',
'To-do tasks', 'Users', 'Workbooks and charts'
];

export const options = z.strictObject({
...globalOptionsZod.shape,
versions: z.string().optional().alias('v'),
changeType: z.string().optional().alias('c'),
services: z.string().optional().alias('s'),
startDate: z.string().optional(),
endDate: z.string().optional()
});

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

interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
versions?: string;
changeType?: string;
services?: string;
startDate?: string;
endDate?: string;
}

class GraphChangelogListCommand extends AnonymousCommand {
private allowedVersions: string[] = ['beta', 'v1.0'];
private allowedChangeTypes: string[] = ['Addition', 'Change', 'Deletion', 'Deprecation'];
private allowedServices: string[] = [
'Applications', 'Calendar', 'Change notifications', 'Cloud communications',
'Compliance', 'Cross-device experiences', 'Customer booking', 'Device and app management',
'Education', 'Files', 'Financials', 'Groups',
'Identity and access', 'Mail', 'Notes', 'Notifications',
'People and workplace intelligence', 'Personal contacts', 'Reports', 'Search',
'Security', 'Sites and lists', 'Tasks and plans', 'Teamwork',
'To-do tasks', 'Users', 'Workbooks and charts'
];

public get name(): string {
return commands.CHANGELOG_LIST;
}
Expand All @@ -46,77 +50,56 @@ class GraphChangelogListCommand extends AnonymousCommand {
return ['category', 'title', 'description'];
}

constructor() {
super();

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

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
versions: typeof args.options.versions !== 'undefined',
changeType: typeof args.options.changeType !== 'undefined',
services: typeof args.options.services !== 'undefined',
startDate: typeof args.options.startDate !== 'undefined',
endDate: typeof args.options.endDate !== 'undefined'
});
});
}

#initOptions(): void {
this.options.unshift(
{ option: '-v, --versions [versions]', autocomplete: this.allowedVersions },
{ option: "-c, --changeType [changeType]", autocomplete: this.allowedChangeTypes },
{ option: "-s, --services [services]", autocomplete: this.allowedServices },
{ option: "--startDate [startDate]" },
{ option: "--endDate [endDate]" }
);
public get schema(): z.ZodType | undefined {
return options;
}

#initValidators(): void {
this.validators.push(
async (args: CommandArgs) => {
if (
args.options.versions &&
args.options.versions.toLocaleLowerCase().split(',').some(x => !this.allowedVersions.map(y => y.toLocaleLowerCase()).includes(x))) {
return `The verions contains an invalid value. Specify either ${this.allowedVersions.join(', ')} as properties`;
public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined {
return schema
.refine(options => {
if (!options.versions) {
return true;
}

if (
args.options.changeType &&
!this.allowedChangeTypes.map(x => x.toLocaleLowerCase()).includes(args.options.changeType.toLocaleLowerCase())) {
return `The change type contain an invalid value. Specify either ${this.allowedChangeTypes.join(', ')} as properties`;
}

if (
args.options.services &&
args.options.services.toLocaleLowerCase().split(',').some(x => !this.allowedServices.map(y => y.toLocaleLowerCase()).includes(x))) {
return `The services contains invalid value. Specify either ${this.allowedServices.join(', ')} as properties`;
}

if (args.options.startDate && !validation.isValidISODate(args.options.startDate)) {
return 'The startDate is not a valid ISO date string';
return !options.versions.toLocaleLowerCase().split(',').some(x => !allowedVersions.map(y => y.toLocaleLowerCase()).includes(x));
}, {
error: `The verions contains an invalid value. Specify either ${allowedVersions.join(', ')} as properties`,
path: ['versions']
})
.refine(options => {
if (!options.changeType) {
return true;
}

if (args.options.endDate && !validation.isValidISODate(args.options.endDate)) {
return 'The endDate is not a valid ISO date string';
}

if (args.options.endDate && args.options.startDate && new Date(args.options.endDate) < new Date(args.options.startDate)) {
return 'The endDate should be later than startDate';
return allowedChangeTypes.map(x => x.toLocaleLowerCase()).includes(options.changeType.toLocaleLowerCase());
}, {
error: `The change type contain an invalid value. Specify either ${allowedChangeTypes.join(', ')} as properties`,
path: ['changeType']
})
.refine(options => {
if (!options.services) {
return true;
}

return true;
}
);
return !options.services.toLocaleLowerCase().split(',').some(x => !allowedServices.map(y => y.toLocaleLowerCase()).includes(x));
}, {
error: `The services contains invalid value. Specify either ${allowedServices.join(', ')} as properties`,
path: ['services']
})
.refine(options => !options.startDate || validation.isValidISODate(options.startDate), {
error: 'The startDate is not a valid ISO date string',
path: ['startDate']
})
.refine(options => !options.endDate || validation.isValidISODate(options.endDate), {
error: 'The endDate is not a valid ISO date string',
path: ['endDate']
})
.refine(options => !(options.endDate && options.startDate && new Date(options.endDate) < new Date(options.startDate)), {
error: 'The endDate should be later than startDate',
path: ['endDate']
});
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
try {
const allowedChangeType = args.options.changeType && this.allowedChangeTypes.find(x => x.toLocaleLowerCase() === args.options.changeType!.toLocaleLowerCase());
const allowedChangeType = args.options.changeType && allowedChangeTypes.find(x => x.toLocaleLowerCase() === args.options.changeType!.toLocaleLowerCase());
const searchParam = args.options.changeType ? `/?filterBy=${allowedChangeType}` : '';

const requestOptions: CliRequestOptions = {
Expand Down Expand Up @@ -144,17 +127,17 @@ class GraphChangelogListCommand extends AnonymousCommand {
let items: ChangelogItem[] = changelog.items;

if (options.services) {
const allowedServices: string[] = this.allowedServices
const matchedServices: string[] = allowedServices
.filter(allowedService => options.services!.toLocaleLowerCase().split(',').includes(allowedService.toLocaleLowerCase()));

items = changelog.items.filter(item => allowedServices.includes(item.title));
items = changelog.items.filter(item => matchedServices.includes(item.title));
}

if (options.versions) {
const allowedVersions: string[] = this.allowedVersions
const matchedVersions: string[] = allowedVersions
.filter(allowedVersion => options.versions!.toLocaleLowerCase().split(',').includes(allowedVersion.toLocaleLowerCase()));

items = items.filter(item => allowedVersions.includes(item.category));
items = items.filter(item => matchedVersions.includes(item.category));
}

if (options.startDate) {
Expand Down
Loading
Loading