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);
Copy link
Contributor

@MartinM85 MartinM85 Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parse the command option from zod schema to ensure the inputs are correct.

Suggested change
await command.action(logger, { options: { id: containerId } } as any);
await command.action(logger, { options: schema.parse({ id: containerId }) } as any);

Please do it for all unit tests.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try to use $filter and return only containers with the specific name?

If filtering by the displayName not supported...rather use the odata.getAllItems which handles also paging and returns all items.

Suggested change
const response = await request.get<{ value?: SpeContainer[] }>(requestOptions);
const response = await odata.getAllItems<SpeContainer[]>(requestOptions);

According to the doc the /v1.0/storage/fileStorage/containers endpoint requires the containerTypeId parameter. Does the query work without the containerTypeId?

In fact, if a user wants to retrieve a container by it's name, we will make two calls. One call to get all containers and find the one with the specific name. The second call to get a container by it's id even if we have already retrieved the container and its details in the first call.

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();