Skip to content
Merged
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
48 changes: 41 additions & 7 deletions src/m365/util/commands/accesstoken/accesstoken-get.spec.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import assert from 'assert';
import sinon from 'sinon';
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 { 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 './accesstoken-get.js';
import command, { options } from './accesstoken-get.js';

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

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').resolves();
sinon.stub(pid, 'getProcessName').returns('');
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 @@ -69,7 +75,7 @@ describe(commands.ACCESSTOKEN_GET, () => {
accessToken: 'ABC'
};

await command.action(logger, { options: { resource: 'https://graph.microsoft.com' } });
await command.action(logger, { options: commandOptionsSchema.parse({ resource: 'https://graph.microsoft.com' }) });
assert(loggerLogSpy.calledWith('ABC'));
});

Expand All @@ -82,14 +88,14 @@ describe(commands.ACCESSTOKEN_GET, () => {
accessToken: 'ABC'
};

await command.action(logger, { options: { resource: 'sharepoint' } });
await command.action(logger, { options: commandOptionsSchema.parse({ resource: 'sharepoint' }) });
assert(loggerLogSpy.calledWith('ABC'));
});

it('correctly handles error when retrieving access token', async () => {
sinon.stub(auth, 'ensureAccessToken').rejects(new Error('An error has occurred'));

await assert.rejects(command.action(logger, { options: { resource: 'https://graph.microsoft.com' } } as any), new CommandError('An error has occurred'));
await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ resource: 'https://graph.microsoft.com' }) }), new CommandError('An error has occurred'));
});

it('returns error when sharepoint specified as resource and SPO URL not available', async () => {
Expand All @@ -100,7 +106,7 @@ describe(commands.ACCESSTOKEN_GET, () => {
accessToken: 'ABC'
};

await assert.rejects(command.action(logger, { options: { resource: 'sharepoint' } } as any), new CommandError(`SharePoint URL undefined. Use the 'm365 spo set --url https://contoso.sharepoint.com' command to set the URL`));
await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ resource: 'sharepoint' }) }), new CommandError(`SharePoint URL undefined. Use the 'm365 spo set --url https://contoso.sharepoint.com' command to set the URL`));
});

it('retrieves access token for graph.microsoft.com when graph specified as the resource', async () => {
Expand All @@ -111,7 +117,7 @@ describe(commands.ACCESSTOKEN_GET, () => {
accessToken: 'ABC'
};

await command.action(logger, { options: { resource: 'graph' } });
await command.action(logger, { options: commandOptionsSchema.parse({ resource: 'graph' }) });
assert(loggerLogSpy.calledWith('ABC'));
});

Expand All @@ -123,7 +129,7 @@ describe(commands.ACCESSTOKEN_GET, () => {
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
};

await command.action(logger, { options: { resource: 'graph', decoded: true } });
await command.action(logger, { options: commandOptionsSchema.parse({ resource: 'graph', decoded: true }) });
assert(loggerLogRawSpy.calledWith(`{
"alg": "HS256",
"typ": "JWT"
Expand All @@ -133,4 +139,32 @@ describe(commands.ACCESSTOKEN_GET, () => {
"iat": 1516239022
}.[signature]`));
});

it('fails validation if resource is missing', () => {
const actual = commandOptionsSchema.safeParse({});
assert.strictEqual(actual.success, false);
});

it('fails validation with unknown options', () => {
const actual = commandOptionsSchema.safeParse({
resource: 'https://graph.microsoft.com',
unknownOption: 'value'
});
assert.strictEqual(actual.success, false);
});

it('passes validation with all required options', () => {
const actual = commandOptionsSchema.safeParse({ resource: 'https://graph.microsoft.com' });
assert.strictEqual(actual.success, true);
});

it('passes validation with optional new flag', () => {
const actual = commandOptionsSchema.safeParse({ resource: 'https://graph.microsoft.com', new: true });
assert.strictEqual(actual.success, true);
});

it('passes validation with optional decoded flag', () => {
const actual = commandOptionsSchema.safeParse({ resource: 'https://graph.microsoft.com', decoded: true });
assert.strictEqual(actual.success, true);
});
});
49 changes: 13 additions & 36 deletions src/m365/util/commands/accesstoken/accesstoken-get.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { z } from 'zod';
import auth, { Auth } from '../../../../Auth.js';
import { Logger } from '../../../../cli/Logger.js';
import Command from '../../../../Command.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import Command, { globalOptionsZod } from '../../../../Command.js';
import commands from '../../commands.js';
import { accessToken } from '../../../../utils/accessToken.js';

export const options = z.strictObject({
...globalOptionsZod.shape,
resource: z.string().alias('r'),
new: z.boolean().optional(),
decoded: z.boolean().optional()
});

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

interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
new?: boolean;
resource: string;
decoded?: boolean;
}

class UtilAccessTokenGetCommand extends Command {
public get name(): string {
return commands.ACCESSTOKEN_GET;
Expand All @@ -24,34 +27,8 @@ class UtilAccessTokenGetCommand extends Command {
return 'Gets access token for the specified resource';
}

constructor() {
super();

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

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
new: args.options.new,
decoded: args.options.decoded
});
});
}

#initOptions(): void {
this.options.unshift(
{
option: '-r, --resource <resource>'
},
{
option: '--new'
},
{
option: '--decoded'
}
);
public get schema(): z.ZodType | undefined {
return options;
}

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