From 19cf370ea18360fd51c1268d318a270daad6749b Mon Sep 17 00:00:00 2001 From: Nirav Raval Date: Fri, 3 Oct 2025 01:35:45 +0000 Subject: [PATCH 1/7] Migrate 'entra license list' to Zod --- .../commands/license/license-list.spec.ts | 18 +++++++++++++++--- .../entra/commands/license/license-list.ts | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/m365/entra/commands/license/license-list.spec.ts b/src/m365/entra/commands/license/license-list.spec.ts index a2ae895e100..35312ef6819 100644 --- a/src/m365/entra/commands/license/license-list.spec.ts +++ b/src/m365/entra/commands/license/license-list.spec.ts @@ -1,8 +1,11 @@ import assert from 'assert'; import sinon from 'sinon'; +import { z } from 'zod'; import auth from '../../../../Auth.js'; -import { CommandError } from '../../../../Command.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'; @@ -65,6 +68,8 @@ describe(commands.LICENSE_LIST, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -72,6 +77,8 @@ describe(commands.LICENSE_LIST, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; }); beforeEach(() => { @@ -109,6 +116,11 @@ describe(commands.LICENSE_LIST, () => { assert.notStrictEqual(command.description, null); }); + it('validates the schema', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, true); + }); + it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'skuId', 'skuPartNumber']); }); @@ -122,7 +134,7 @@ describe(commands.LICENSE_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({}) }); assert(loggerLogSpy.calledWith(licenseResponse.value)); }); @@ -136,7 +148,7 @@ describe(commands.LICENSE_LIST, () => { sinon.stub(request, 'get').rejects(error); await assert.rejects(command.action(logger, { - options: {} + options: commandOptionsSchema.parse({}) }), new CommandError(error.error.message)); }); }); diff --git a/src/m365/entra/commands/license/license-list.ts b/src/m365/entra/commands/license/license-list.ts index b3bdcc9dfba..c33dc4c8917 100644 --- a/src/m365/entra/commands/license/license-list.ts +++ b/src/m365/entra/commands/license/license-list.ts @@ -1,7 +1,16 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +import { globalOptionsZod } from '../../../../Command.js'; + +const options = globalOptionsZod.strict(); +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} class EntraLicenseListCommand extends GraphCommand { public get name(): string { @@ -12,11 +21,15 @@ class EntraLicenseListCommand extends GraphCommand { return 'Lists commercial subscriptions that an organization has acquired'; } + public get schema(): z.ZodTypeAny | undefined { + return options; + } + public defaultProperties(): string[] | undefined { return ['id', 'skuId', 'skuPartNumber']; } - public async commandAction(logger: Logger): Promise { + public async commandAction(logger: Logger, _args: CommandArgs): Promise { if (this.verbose) { await logger.logToStderr(`Retrieving the commercial subscriptions that an organization has acquired`); } From 1d463f4fca125c5730aac33eb9715e58f802ba1b Mon Sep 17 00:00:00 2001 From: Nirav Raval Date: Fri, 3 Oct 2025 03:47:24 +0000 Subject: [PATCH 2/7] fix license retrieval tests --- .../commands/license/license-list.spec.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/m365/entra/commands/license/license-list.spec.ts b/src/m365/entra/commands/license/license-list.spec.ts index 35312ef6819..951bee56bd1 100644 --- a/src/m365/entra/commands/license/license-list.spec.ts +++ b/src/m365/entra/commands/license/license-list.spec.ts @@ -68,6 +68,7 @@ describe(commands.LICENSE_LIST, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let loggerStderrSpy: sinon.SinonSpy; let commandInfo: CommandInfo; let commandOptionsSchema: z.ZodTypeAny; @@ -95,6 +96,7 @@ describe(commands.LICENSE_LIST, () => { } }; loggerLogSpy = sinon.spy(logger, 'log'); + loggerStderrSpy = sinon.spy(logger, 'logToStderr'); }); afterEach(() => { @@ -125,18 +127,47 @@ describe(commands.LICENSE_LIST, () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'skuId', 'skuPartNumber']); }); - it('retrieves licenses', async () => { + it('uses default properties when retrieving licenses', async () => { sinon.stub(request, 'get').callsFake(async opts => { if ((opts.url === `https://graph.microsoft.com/v1.0/subscribedSkus`)) { return licenseResponse; } + throw 'Invalid request'; + }); + + await command.action(logger, { options: { output: 'json', query: '' } }); + + assert(loggerLogSpy.calledOnce); + const loggedValue = loggerLogSpy.firstCall.args[0]; + assert(Array.isArray(loggedValue)); + + const defaultProps = command.defaultProperties(); + loggedValue.forEach(license => { + // Check all default properties exist + defaultProps?.forEach(prop => { + assert(license.hasOwnProperty(prop), `License should have ${prop} property`); + }); + }); + }); + it('retrieves licenses', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if ((opts.url === `https://graph.microsoft.com/v1.0/subscribedSkus`)) { + return licenseResponse; + } throw 'Invalid request'; }); await command.action(logger, { options: commandOptionsSchema.parse({}) }); assert(loggerLogSpy.calledWith(licenseResponse.value)); + }); + + it('logs retrieving message in verbose mode', async () => { + sinon.stub(request, 'get').resolves({ value: [] }); + + await command.action(logger, { options: { verbose: true } }); + assert(loggerStderrSpy.calledWith('Retrieving the commercial subscriptions that an organization has acquired')); }); it('correctly handles random API error', async () => { From ed94f6183dce54a833708e8eebd5e8d4ea0fe934 Mon Sep 17 00:00:00 2001 From: Nirav Raval Date: Fri, 3 Oct 2025 01:35:45 +0000 Subject: [PATCH 3/7] Migrate 'entra license list' to Zod --- .../commands/license/license-list.spec.ts | 18 +++++++++++++++--- .../entra/commands/license/license-list.ts | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/m365/entra/commands/license/license-list.spec.ts b/src/m365/entra/commands/license/license-list.spec.ts index a2ae895e100..35312ef6819 100644 --- a/src/m365/entra/commands/license/license-list.spec.ts +++ b/src/m365/entra/commands/license/license-list.spec.ts @@ -1,8 +1,11 @@ import assert from 'assert'; import sinon from 'sinon'; +import { z } from 'zod'; import auth from '../../../../Auth.js'; -import { CommandError } from '../../../../Command.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'; @@ -65,6 +68,8 @@ describe(commands.LICENSE_LIST, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -72,6 +77,8 @@ describe(commands.LICENSE_LIST, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; }); beforeEach(() => { @@ -109,6 +116,11 @@ describe(commands.LICENSE_LIST, () => { assert.notStrictEqual(command.description, null); }); + it('validates the schema', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, true); + }); + it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'skuId', 'skuPartNumber']); }); @@ -122,7 +134,7 @@ describe(commands.LICENSE_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({}) }); assert(loggerLogSpy.calledWith(licenseResponse.value)); }); @@ -136,7 +148,7 @@ describe(commands.LICENSE_LIST, () => { sinon.stub(request, 'get').rejects(error); await assert.rejects(command.action(logger, { - options: {} + options: commandOptionsSchema.parse({}) }), new CommandError(error.error.message)); }); }); diff --git a/src/m365/entra/commands/license/license-list.ts b/src/m365/entra/commands/license/license-list.ts index b3bdcc9dfba..c33dc4c8917 100644 --- a/src/m365/entra/commands/license/license-list.ts +++ b/src/m365/entra/commands/license/license-list.ts @@ -1,7 +1,16 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +import { globalOptionsZod } from '../../../../Command.js'; + +const options = globalOptionsZod.strict(); +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} class EntraLicenseListCommand extends GraphCommand { public get name(): string { @@ -12,11 +21,15 @@ class EntraLicenseListCommand extends GraphCommand { return 'Lists commercial subscriptions that an organization has acquired'; } + public get schema(): z.ZodTypeAny | undefined { + return options; + } + public defaultProperties(): string[] | undefined { return ['id', 'skuId', 'skuPartNumber']; } - public async commandAction(logger: Logger): Promise { + public async commandAction(logger: Logger, _args: CommandArgs): Promise { if (this.verbose) { await logger.logToStderr(`Retrieving the commercial subscriptions that an organization has acquired`); } From 1da828b40cc490895a9cc10090548ef9ce40686a Mon Sep 17 00:00:00 2001 From: Nirav Raval Date: Fri, 3 Oct 2025 03:47:24 +0000 Subject: [PATCH 4/7] fix license retrieval tests --- .../commands/license/license-list.spec.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/m365/entra/commands/license/license-list.spec.ts b/src/m365/entra/commands/license/license-list.spec.ts index 35312ef6819..951bee56bd1 100644 --- a/src/m365/entra/commands/license/license-list.spec.ts +++ b/src/m365/entra/commands/license/license-list.spec.ts @@ -68,6 +68,7 @@ describe(commands.LICENSE_LIST, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let loggerStderrSpy: sinon.SinonSpy; let commandInfo: CommandInfo; let commandOptionsSchema: z.ZodTypeAny; @@ -95,6 +96,7 @@ describe(commands.LICENSE_LIST, () => { } }; loggerLogSpy = sinon.spy(logger, 'log'); + loggerStderrSpy = sinon.spy(logger, 'logToStderr'); }); afterEach(() => { @@ -125,18 +127,47 @@ describe(commands.LICENSE_LIST, () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'skuId', 'skuPartNumber']); }); - it('retrieves licenses', async () => { + it('uses default properties when retrieving licenses', async () => { sinon.stub(request, 'get').callsFake(async opts => { if ((opts.url === `https://graph.microsoft.com/v1.0/subscribedSkus`)) { return licenseResponse; } + throw 'Invalid request'; + }); + + await command.action(logger, { options: { output: 'json', query: '' } }); + + assert(loggerLogSpy.calledOnce); + const loggedValue = loggerLogSpy.firstCall.args[0]; + assert(Array.isArray(loggedValue)); + + const defaultProps = command.defaultProperties(); + loggedValue.forEach(license => { + // Check all default properties exist + defaultProps?.forEach(prop => { + assert(license.hasOwnProperty(prop), `License should have ${prop} property`); + }); + }); + }); + it('retrieves licenses', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if ((opts.url === `https://graph.microsoft.com/v1.0/subscribedSkus`)) { + return licenseResponse; + } throw 'Invalid request'; }); await command.action(logger, { options: commandOptionsSchema.parse({}) }); assert(loggerLogSpy.calledWith(licenseResponse.value)); + }); + + it('logs retrieving message in verbose mode', async () => { + sinon.stub(request, 'get').resolves({ value: [] }); + + await command.action(logger, { options: { verbose: true } }); + assert(loggerStderrSpy.calledWith('Retrieving the commercial subscriptions that an organization has acquired')); }); it('correctly handles random API error', async () => { From d78c86eba2a46f3ac1471fae8a8f6190e444bc3d Mon Sep 17 00:00:00 2001 From: Nirav Date: Sun, 19 Oct 2025 18:04:47 -0400 Subject: [PATCH 5/7] apply review feedback and cleanup --- .../commands/license/license-list.spec.ts | 29 ++----------------- .../entra/commands/license/license-list.ts | 6 +--- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/src/m365/entra/commands/license/license-list.spec.ts b/src/m365/entra/commands/license/license-list.spec.ts index 951bee56bd1..dd4990ebbee 100644 --- a/src/m365/entra/commands/license/license-list.spec.ts +++ b/src/m365/entra/commands/license/license-list.spec.ts @@ -118,7 +118,7 @@ describe(commands.LICENSE_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('validates the schema', () => { + it('passes validation with no options', () => { const actual = commandOptionsSchema.safeParse({}); assert.strictEqual(actual.success, true); }); @@ -127,29 +127,6 @@ describe(commands.LICENSE_LIST, () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'skuId', 'skuPartNumber']); }); - it('uses default properties when retrieving licenses', async () => { - sinon.stub(request, 'get').callsFake(async opts => { - if ((opts.url === `https://graph.microsoft.com/v1.0/subscribedSkus`)) { - return licenseResponse; - } - throw 'Invalid request'; - }); - - await command.action(logger, { options: { output: 'json', query: '' } }); - - assert(loggerLogSpy.calledOnce); - const loggedValue = loggerLogSpy.firstCall.args[0]; - assert(Array.isArray(loggedValue)); - - const defaultProps = command.defaultProperties(); - loggedValue.forEach(license => { - // Check all default properties exist - defaultProps?.forEach(prop => { - assert(license.hasOwnProperty(prop), `License should have ${prop} property`); - }); - }); - }); - it('retrieves licenses', async () => { sinon.stub(request, 'get').callsFake(async opts => { if ((opts.url === `https://graph.microsoft.com/v1.0/subscribedSkus`)) { @@ -158,14 +135,14 @@ describe(commands.LICENSE_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: commandOptionsSchema.parse({}) }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true }) }); assert(loggerLogSpy.calledWith(licenseResponse.value)); }); it('logs retrieving message in verbose mode', async () => { sinon.stub(request, 'get').resolves({ value: [] }); - await command.action(logger, { options: { verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true }) }); assert(loggerStderrSpy.calledWith('Retrieving the commercial subscriptions that an organization has acquired')); }); diff --git a/src/m365/entra/commands/license/license-list.ts b/src/m365/entra/commands/license/license-list.ts index c33dc4c8917..d88c02a3c16 100644 --- a/src/m365/entra/commands/license/license-list.ts +++ b/src/m365/entra/commands/license/license-list.ts @@ -6,11 +6,7 @@ import commands from '../../commands.js'; import { globalOptionsZod } from '../../../../Command.js'; const options = globalOptionsZod.strict(); -declare type Options = z.infer; -interface CommandArgs { - options: Options; -} class EntraLicenseListCommand extends GraphCommand { public get name(): string { @@ -29,7 +25,7 @@ class EntraLicenseListCommand extends GraphCommand { return ['id', 'skuId', 'skuPartNumber']; } - public async commandAction(logger: Logger, _args: CommandArgs): Promise { + public async commandAction(logger: Logger): Promise { if (this.verbose) { await logger.logToStderr(`Retrieving the commercial subscriptions that an organization has acquired`); } From 72e1e20197d2028cd7e8f7da2b8622cc0f2ef934 Mon Sep 17 00:00:00 2001 From: Nirav Raval Date: Sun, 19 Oct 2025 18:11:09 -0400 Subject: [PATCH 6/7] Update src/m365/entra/commands/license/license-list.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/m365/entra/commands/license/license-list.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/m365/entra/commands/license/license-list.ts b/src/m365/entra/commands/license/license-list.ts index d88c02a3c16..a76f46c8dc4 100644 --- a/src/m365/entra/commands/license/license-list.ts +++ b/src/m365/entra/commands/license/license-list.ts @@ -7,7 +7,6 @@ import { globalOptionsZod } from '../../../../Command.js'; const options = globalOptionsZod.strict(); - class EntraLicenseListCommand extends GraphCommand { public get name(): string { return commands.LICENSE_LIST; From 9f62691b8ed06e9d0cb32a1667ac3db0a38134d3 Mon Sep 17 00:00:00 2001 From: Nirav Raval Date: Sun, 19 Oct 2025 18:11:35 -0400 Subject: [PATCH 7/7] Update src/m365/entra/commands/license/license-list.spec.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/m365/entra/commands/license/license-list.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m365/entra/commands/license/license-list.spec.ts b/src/m365/entra/commands/license/license-list.spec.ts index dd4990ebbee..bc60a8ce56d 100644 --- a/src/m365/entra/commands/license/license-list.spec.ts +++ b/src/m365/entra/commands/license/license-list.spec.ts @@ -132,7 +132,7 @@ describe(commands.LICENSE_LIST, () => { if ((opts.url === `https://graph.microsoft.com/v1.0/subscribedSkus`)) { return licenseResponse; } - throw 'Invalid request'; + throw new Error('Invalid request'); }); await command.action(logger, { options: commandOptionsSchema.parse({ debug: true }) });