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
15 changes: 12 additions & 3 deletions docs/docs/cmd/spe/container/container-get.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ m365 spe container get [options]
## Options

```md definition-list
`-i, --id <id>`
: The Id of the container instance.
`-i, --id [id]`
: The Id of the container instance. Specify either `id` or `name` but not both.

`-n, --name [name]`
: Display name of the container. Specify either `id` or `name` but not both.
```

<Global />
Expand All @@ -42,12 +45,18 @@ m365 spe container get [options]

## Examples

Gets a container of a specific type.
Gets a container of a specific type by id.

```sh
m365 spe container get --id "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z"
```

Gets a container of a specific type by display name.

```sh
m365 spe container get --name "My Application Storage Container"
```

## Response

<Tabs>
Expand Down
117 changes: 98 additions & 19 deletions src/m365/spe/commands/container/container-get.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'assert';
import sinon from 'sinon';
import { z } from 'zod';
import auth from '../../../../Auth.js';
import { Logger } from '../../../../cli/Logger.js';
import { CommandError } from '../../../../Command.js';
Expand All @@ -12,16 +13,32 @@ import commands from '../../commands.js';
import command from './container-get.js';

describe(commands.CONTAINER_GET, () => {
const containerId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiIxNTU1MjcwOTQyNzIifQ';
const containerName = 'My Application Storage Container';
const containerResponse = {
id: containerId,
displayName: containerName,
description: 'Description of My Application Storage Container',
containerTypeId: '91710488-5756-407f-9046-fbe5f0b4de73',
status: 'active',
createdDateTime: '2021-11-24T15:41:52.347Z',
settings: {
isOcrEnabled: false
}
};
let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let loggerLogToStderrSpy: sinon.SinonSpy;
let schema: z.ZodTypeAny;

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;
schema = command.getSchemaToParse()!;
});

beforeEach(() => {
Expand All @@ -38,12 +55,13 @@ describe(commands.CONTAINER_GET, () => {
}
};
loggerLogSpy = sinon.spy(logger, 'log');
loggerLogToStderrSpy = sinon.spy(logger, 'logToStderr');
});

afterEach(() => {
sinonUtil.restore([
request.get
]);
loggerLogSpy.restore();
loggerLogToStderrSpy.restore();
sinonUtil.restore([request.get]);
});

after(() => {
Expand Down Expand Up @@ -72,28 +90,89 @@ describe(commands.CONTAINER_GET, () => {
});

it('gets container by id', async () => {
const containerId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiIxNTU1MjcwOTQyNzIifQ';
const response = {
id: containerId,
displayName: "My Application Storage Container",
description: "Description of My Application Storage Container",
containerTypeId: "91710488-5756-407f-9046-fbe5f0b4de73",
status: "active",
createdDateTime: "2021-11-24T15:41:52.347Z",
settings: {
isOcrEnabled: false
}
};

sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${containerId}`) {
return response;
return containerResponse;
}

throw 'Invalid Request';
});

await command.action(logger, { options: { id: containerId } } as any);
assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], response);
assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], containerResponse);
});

it('gets container by name', async () => {
sinon.stub(request, 'get').onFirstCall().resolves({
value: [containerResponse]
}).onSecondCall().callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${containerId}`) {
return containerResponse;
}

throw 'Invalid Request';
});

await command.action(logger, { options: { name: containerName } } as any);
assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], containerResponse);
});

it('fails when container with specified name does not exist', async () => {
sinon.stub(request, 'get').resolves({ value: [] });

await assert.rejects(
command.action(logger, { options: { name: containerName } } as any),
new CommandError(`Container with name '${containerName}' not found.`)
);
});

it('logs progress when resolving container id by name in verbose mode', async () => {
sinon.stub(request, 'get').onFirstCall().resolves({
value: [containerResponse]
}).onSecondCall().callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${containerId}`) {
return containerResponse;
}

throw 'Invalid Request';
});

await command.action(logger, { options: { name: containerName, verbose: true } } as any);
assert(loggerLogToStderrSpy.calledWith(`Resolving container id from name '${containerName}'...`));
});

it('throws received error when resolving container id fails with unexpected error', async () => {
const unexpectedError = new Error('Unexpected');
sinon.stub(request, 'get').rejects(unexpectedError);

try {
await command.action(logger, { options: { name: containerName } } as any);
assert.fail('Expected command to throw');
}
catch (err: any) {
assert.strictEqual(err.message, unexpectedError.message);
}
});

it('fails validation when neither id nor name is specified', () => {
const result = schema.safeParse({});
assert.strictEqual(result.success, false);
assert(result.error?.issues.some(issue => issue.message.includes('Specify either id or name')));
});

it('fails validation when both id and name are specified', () => {
const result = schema.safeParse({ id: containerId, name: containerName });
assert.strictEqual(result.success, false);
assert(result.error?.issues.some(issue => issue.message.includes('Specify either id or name')));
});

it('passes validation when only id is specified', () => {
const result = schema.safeParse({ id: containerId });
assert.strictEqual(result.success, true);
});

it('passes validation when only name is specified', () => {
const result = schema.safeParse({ name: containerName });
assert.strictEqual(result.success, true);
});
});
});
88 changes: 64 additions & 24 deletions src/m365/spe/commands/container/container-get.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import GlobalOptions from '../../../../GlobalOptions.js';
import { z } from 'zod';
import { Logger } from '../../../../cli/Logger.js';
import { CommandError, globalOptionsZod } from '../../../../Command.js';
import GraphCommand from '../../../base/GraphCommand.js';
import commands from '../../commands.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { SpeContainer } from '../../../../utils/spe.js';
import { zod } from '../../../../utils/zod.js';

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

type Options = z.infer<typeof options>;

interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
id: string;
}

class SpeContainerGetCommand extends GraphCommand {
public get name(): string {
return commands.CONTAINER_GET;
Expand All @@ -22,44 +27,79 @@ class SpeContainerGetCommand extends GraphCommand {
return 'Gets a container of a specific container type';
}

constructor() {
super();

this.#initOptions();
this.#initTypes();
public get schema(): z.ZodTypeAny {
return options;
}

#initOptions(): void {
this.options.unshift(
{ option: '-i, --id <id>' }
);
public getRefinedSchema(schema: z.ZodTypeAny): z.ZodEffects<any> | undefined {
return schema.refine((opts: Options) => [opts.id, opts.name].filter(value => value !== undefined).length === 1, {
message: 'Specify either id or name, but not both.'
});
}

#initTypes(): void {
this.types.string.push('id');
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
try {
const containerId = await this.resolveContainerId(args.options, logger);

if (this.verbose) {
await logger.logToStderr(`Getting a container with id '${containerId}'...`);
}

const requestOptions: CliRequestOptions = {
url: `${this.resource}/v1.0/storage/fileStorage/containers/${containerId}`,
headers: {
accept: 'application/json;odata.metadata=none'
},
responseType: 'json'
};

const res = await request.get<SpeContainer>(requestOptions);
await logger.log(res);
}
catch (err: any) {
if (err instanceof CommandError) {
throw err;
}

this.handleRejectedODataJsonPromise(err);
}
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
private async resolveContainerId(options: Options, logger: Logger): Promise<string> {
if (options.id) {
return options.id;
}

if (this.verbose) {
await logger.logToStderr(`Getting a container with id '${args.options.id}'...`);
await logger.logToStderr(`Resolving container id from name '${options.name}'...`);
}

const requestOptions: CliRequestOptions = {
url: `${this.resource}/v1.0/storage/fileStorage/containers/${args.options.id}`,
url: `${this.resource}/v1.0/storage/fileStorage/containers`,
headers: {
accept: 'application/json;odata.metadata=none'
},
responseType: 'json'
};

try {
const res = await request.get<SpeContainer>(requestOptions);
await logger.log(res);
const response = await request.get<{ value?: SpeContainer[] }>(requestOptions);
const container = response.value?.find(item => item.displayName === options.name);

if (!container) {
throw new CommandError(`Container with name '${options.name}' not found.`);
}

return container.id;
}
catch (err: any) {
this.handleRejectedODataJsonPromise(err);
catch (error: any) {
if (error instanceof CommandError) {
throw error;
}

throw error;
}
}
}

export default new SpeContainerGetCommand();
export default new SpeContainerGetCommand();