diff --git a/docs/docs/cmd/spe/container/container-get.mdx b/docs/docs/cmd/spe/container/container-get.mdx
index d9e118ea3e1..054b5b3b1d4 100644
--- a/docs/docs/cmd/spe/container/container-get.mdx
+++ b/docs/docs/cmd/spe/container/container-get.mdx
@@ -20,6 +20,12 @@ m365 spe container get [options]
`-n, --name [name]`
: Display name of the container. Specify either `id` or `name` but not both.
+
+`--containerTypeId [containerTypeId]`
+: The container type ID of the container instance. Specify either `containerTypeId` or `containerTypeName` when using `name` but not both.
+
+`--containerTypeName [containerTypeName]`
+: The container type name of the container instance. Specify either `containerTypeId` or `containerTypeName` when using `name` but not both.
```
@@ -54,7 +60,13 @@ m365 spe container get --id "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDE
Gets a container of a specific type by display name.
```sh
-m365 spe container get --name "My Application Storage Container"
+m365 spe container get --name "My Application Storage Container" --containerTypeId "91710488-5756-407f-9046-fbe5f0b4de73"
+```
+
+Gets container by using its name and container type name.
+
+```sh
+m365 spe container get --name "Invoices" --containerTypeName "My container type name"
```
## Response
diff --git a/docs/docs/cmd/spe/container/container-permission-list.mdx b/docs/docs/cmd/spe/container/container-permission-list.mdx
index ad7a25bc701..427e2d880d7 100644
--- a/docs/docs/cmd/spe/container/container-permission-list.mdx
+++ b/docs/docs/cmd/spe/container/container-permission-list.mdx
@@ -15,8 +15,17 @@ m365 spe container permission list [options]
## Options
```md definition-list
-`-i, --containerId `
-: The ID of the SharePoint Embedded Container.
+`-i, --containerId [id]`
+: The ID of the SharePoint Embedded Container. Specify either `containerId` or `containerName` but not both.
+
+`-n, --containerName [containerName]`
+: Display name of the SharePoint Embedded Container. Specify either `containerId` or `containerName` but not both.
+
+`--containerTypeId [containerTypeId]`
+: The container type ID of the container instance. Specify either `containerTypeId` or `containerTypeName` when using `containerName` but not both.
+
+`--containerTypeName [containerTypeName]`
+: The container type name of the container instance. Specify either `containerTypeId` or `containerTypeName` when using `containerName` but not both.
```
@@ -42,12 +51,24 @@ m365 spe container permission list [options]
## Examples
-Lists Container permissions.
+Lists Container permissions by id.
```sh
m365 spe container permission list --containerId "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z"
```
+Lists Container permissions by display name.
+
+```sh
+m365 spe container permission list --containerName "My Application Storage Container" --containerTypeId "91710488-5756-407f-9046-fbe5f0b4de73"
+```
+
+Lists Container permissions by display name and container type name.
+
+```sh
+m365 spe container permission list --containerName "My Application Storage Container" --containerTypeName "My container type name"
+```
+
## Response
diff --git a/src/m365/spe/commands/container/container-permission-list.spec.ts b/src/m365/spe/commands/container/container-permission-list.spec.ts
index 32cf0695669..82c1f41d8e8 100644
--- a/src/m365/spe/commands/container/container-permission-list.spec.ts
+++ b/src/m365/spe/commands/container/container-permission-list.spec.ts
@@ -3,20 +3,26 @@ import sinon from 'sinon';
import auth from '../../../../Auth.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';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './container-permission-list.js';
-import { formatting } from '../../../../utils/formatting.js';
+import { z } from 'zod';
+import { spe } from '../../../../utils/spe.js';
+import { odata } from '../../../../utils/odata.js';
+import { cli } from '../../../../cli/cli.js';
describe(commands.CONTAINER_PERMISSION_LIST, () => {
let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
+ let loggerLogToStderrSpy: sinon.SinonSpy;
+ let schema: z.ZodTypeAny;
const containerId = "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z";
+ const containerName = 'My Application Storage Container';
+ const containerTypeId = 'b2e2cef4-9ac1-4b3b-b4a5-2a2e3a2e2a2e';
const containerPermissionResponse = {
"value": [
{
@@ -66,6 +72,7 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => {
sinon.stub(telemetry, 'trackEvent').resolves();
sinon.stub(pid, 'getProcessName').returns('');
auth.connection.active = true;
+ schema = command.getSchemaToParse()!;
});
beforeEach(() => {
@@ -82,12 +89,18 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => {
}
};
loggerLogSpy = sinon.spy(logger, 'log');
+ loggerLogToStderrSpy = sinon.spy(logger, 'logToStderr');
});
afterEach(() => {
sinonUtil.restore([
- request.get
+ spe.getContainerIdByName,
+ spe.getContainerTypeIdByName,
+ odata.getAllItems,
+ cli.handleMultipleResultsFound
]);
+ loggerLogSpy.restore();
+ loggerLogToStderrSpy.restore();
});
after(() => {
@@ -108,13 +121,7 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => {
});
it('correctly lists permissions of a SharePoint Embedded Container', async () => {
- sinon.stub(request, 'get').callsFake(async (opts) => {
- if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions`) {
- return containerPermissionResponse;
- }
-
- throw 'Invalid request';
- });
+ sinon.stub(odata, 'getAllItems').resolves(containerPermissionResponse.value);
await command.action(logger, {
options: {
@@ -127,13 +134,7 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => {
});
it('correctly lists permissions of a SharePoint Embedded Container (TEXT)', async () => {
- sinon.stub(request, 'get').callsFake(async (opts) => {
- if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions`) {
- return containerPermissionResponse;
- }
-
- throw 'Invalid request';
- });
+ sinon.stub(odata, 'getAllItems').resolves(containerPermissionResponse.value);
await command.action(logger, {
options: {
@@ -146,8 +147,114 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => {
assert(loggerLogSpy.calledWith(textOutput));
});
+ it('correctly lists permissions of a SharePoint Embedded Container by name', async () => {
+ sinon.stub(odata, 'getAllItems').onFirstCall().resolves([
+ {
+ id: containerId,
+ displayName: containerName
+ }
+ ]).onSecondCall().resolves(containerPermissionResponse.value);
+
+ await command.action(logger, {
+ options: {
+ containerName,
+ containerTypeId,
+ debug: true
+ }
+ });
+
+ assert(loggerLogSpy.calledWith(containerPermissionResponse.value));
+ });
+
+ it('logs progress when resolving container id by name in verbose mode', async () => {
+ sinon.stub(odata, 'getAllItems').onFirstCall().resolves([
+ {
+ id: containerId,
+ displayName: containerName
+ }
+ ]).onSecondCall().resolves(containerPermissionResponse.value);
+
+ await command.action(logger, {
+ options: {
+ containerName,
+ containerTypeId,
+ verbose: true
+ }
+ });
+
+ assert(loggerLogToStderrSpy.calledWith(`Resolving container id from name '${containerName}'...`));
+ });
+
+ it('fails when container with specified name does not exist', async () => {
+ sinon.stub(odata, 'getAllItems').resolves([]);
+
+ await assert.rejects(
+ command.action(logger, {
+ options: {
+ containerName,
+ containerTypeId
+ }
+ }),
+ new CommandError(`The specified container '${containerName}' does not exist.`)
+ );
+ });
+
+ it('handles multiple containers with same name when resolving id', async () => {
+ sinon.stub(odata, 'getAllItems').onFirstCall().resolves([
+ {
+ id: '1',
+ displayName: containerName
+ },
+ {
+ id: containerId,
+ displayName: containerName
+ }
+ ]).onSecondCall().resolves(containerPermissionResponse.value);
+ sinon.stub(cli, 'handleMultipleResultsFound').resolves({
+ id: containerId
+ });
+
+ await command.action(logger, {
+ options: {
+ containerName,
+ containerTypeId
+ }
+ });
+
+ assert(loggerLogSpy.calledWith(containerPermissionResponse.value));
+ });
+
+ it('rethrows unexpected errors when resolving container id by name', async () => {
+ sinon.stub(odata, 'getAllItems').rejects({
+ error: {
+ 'odata.error': {
+ message: {
+ value: 'unexpected error'
+ }
+ }
+ }
+ });
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ containerName,
+ containerTypeId
+ }
+ }), new CommandError('unexpected error'));
+ });
+
+ it('rethrows CommandError thrown during command execution', async () => {
+ sinon.stub(odata, 'getAllItems').rejects(new CommandError('command error'));
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ containerId
+ }
+ }), new CommandError('command error'));
+ });
+
it('correctly handles error when SharePoint Embedded Container is not found', async () => {
- sinon.stub(request, 'get').rejects({
+ sinon.stub(odata, 'getAllItems').rejects({
error: { 'odata.error': { message: { value: 'Item Not Found.' } } }
});
@@ -157,7 +264,7 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => {
it('correctly handles error when retrieving permissions of a SharePoint Embedded Container', async () => {
const error = 'An error has occurred';
- sinon.stub(request, 'get').rejects(new Error(error));
+ sinon.stub(odata, 'getAllItems').rejects(new Error(error));
await assert.rejects(command.action(logger, {
options: {
@@ -165,4 +272,70 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => {
}
}), new CommandError(error));
});
-});
\ No newline at end of file
+
+ it('fails validation when neither containerId nor containerName 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 containerId and containerName are specified', () => {
+ const result = schema.safeParse({
+ containerId,
+ 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 containerId is specified', () => {
+ const result = schema.safeParse({ containerId });
+ assert.strictEqual(result.success, true);
+ });
+
+ it('passes validation when containerName and containerTypeId are specified', () => {
+ const result = schema.safeParse({ containerName, containerTypeId });
+ assert.strictEqual(result.success, true);
+ });
+
+ it('correctly lists permissions of a SharePoint Embedded Container by containerTypeName', async () => {
+ const containerTypeName = 'My Container Type';
+ sinon.stub(spe, 'getContainerTypeIdByName').resolves(containerTypeId);
+ sinon.stub(odata, 'getAllItems').onFirstCall().resolves([
+ {
+ id: containerId,
+ displayName: containerName
+ }
+ ]).onSecondCall().resolves(containerPermissionResponse.value);
+
+ await command.action(logger, {
+ options: {
+ containerName,
+ containerTypeName
+ }
+ });
+
+ assert(loggerLogSpy.calledWith(containerPermissionResponse.value));
+ });
+
+ it('logs progress when getting container type by name in verbose mode', async () => {
+ const containerTypeName = 'My Container Type';
+ sinon.stub(spe, 'getContainerTypeIdByName').resolves(containerTypeId);
+ sinon.stub(odata, 'getAllItems').onFirstCall().resolves([
+ {
+ id: containerId,
+ displayName: containerName
+ }
+ ]).onSecondCall().resolves(containerPermissionResponse.value);
+
+ await command.action(logger, {
+ options: {
+ containerName,
+ containerTypeName,
+ verbose: true
+ }
+ });
+
+ assert(loggerLogToStderrSpy.calledWith(`Getting container type with name '${containerTypeName}'...`));
+ });
+});
diff --git a/src/m365/spe/commands/container/container-permission-list.ts b/src/m365/spe/commands/container/container-permission-list.ts
index 63111bab0ae..6a8c4497b7a 100644
--- a/src/m365/spe/commands/container/container-permission-list.ts
+++ b/src/m365/spe/commands/container/container-permission-list.ts
@@ -1,16 +1,21 @@
import { cli } from '../../../../cli/cli.js';
import { z } from 'zod';
import { Logger } from '../../../../cli/Logger.js';
-import { globalOptionsZod } from '../../../../Command.js';
+import { CommandError, globalOptionsZod } from '../../../../Command.js';
import commands from '../../commands.js';
import GraphCommand from '../../../base/GraphCommand.js';
import { odata } from '../../../../utils/odata.js';
import { formatting } from '../../../../utils/formatting.js';
+import { spe } from '../../../../utils/spe.js';
export const options = z.strictObject({
...globalOptionsZod.shape,
- containerId: z.string().alias('i')
+ containerId: z.string().alias('i').optional(),
+ containerName: z.string().alias('n').optional(),
+ containerTypeId: z.uuid().optional(),
+ containerTypeName: z.string().optional()
});
+
declare type Options = z.infer;
interface CommandArgs {
@@ -34,13 +39,28 @@ class SpeContainerPermissionListCommand extends GraphCommand {
return options;
}
+ public getRefinedSchema(schema: typeof options): z.ZodObject | undefined {
+ return schema
+ .refine((opts: Options) => [opts.containerId, opts.containerName].filter(value => value !== undefined).length === 1, {
+ message: 'Specify either id or name, but not both.'
+ })
+ .refine((options: Options) => !options.containerName || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 1, {
+ error: 'Use one of the following options when specifying the container name: containerTypeId or containerTypeName.'
+ })
+ .refine((options: Options) => options.containerName || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 0, {
+ error: 'Options containerTypeId and containerTypeName are only required when retrieving a container by name.'
+ });
+ }
+
public async commandAction(logger: Logger, args: CommandArgs): Promise {
try {
+ const containerId = await this.resolveContainerId(args.options, logger);
+
if (this.verbose) {
- await logger.logToStderr(`Retrieving permissions of a SharePoint Embedded Container with id '${args.options.containerId}'...`);
+ await logger.logToStderr(`Retrieving permissions of a SharePoint Embedded Container with id '${containerId}'...`);
}
- const containerPermission = await odata.getAllItems(`${this.resource}/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(args.options.containerId)}/permissions`);
+ const containerPermission = await odata.getAllItems(`${this.resource}/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions`);
if (!cli.shouldTrimOutput(args.options.output)) {
await logger.log(containerPermission);
@@ -56,9 +76,37 @@ class SpeContainerPermissionListCommand extends GraphCommand {
}
}
catch (err: any) {
+ if (err instanceof CommandError) {
+ throw err;
+ }
this.handleRejectedODataJsonPromise(err);
}
}
+
+ private async resolveContainerId(options: Options, logger: Logger): Promise {
+ if (options.containerId) {
+ return options.containerId;
+ }
+
+ if (this.verbose) {
+ await logger.logToStderr(`Resolving container id from name '${options.containerName}'...`);
+ }
+
+ const containerTypeId = await this.getContainerTypeId(options, logger);
+ return spe.getContainerIdByName(containerTypeId, options.containerName!);
+ }
+
+ private async getContainerTypeId(options: Options, logger: Logger): Promise {
+ if (options.containerTypeId) {
+ return options.containerTypeId;
+ }
+
+ if (this.verbose) {
+ await logger.logToStderr(`Getting container type with name '${options.containerTypeName}'...`);
+ }
+
+ return spe.getContainerTypeIdByName(options.containerTypeName!);
+ }
}
-export default new SpeContainerPermissionListCommand();
\ No newline at end of file
+export default new SpeContainerPermissionListCommand();