Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds 'spo listitem attachment get' command. Closes #5221 #5332

Closed
wants to merge 7 commits into from
Closed
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
104 changes: 104 additions & 0 deletions docs/docs/cmd/spo/listitem/listitem-attachment-get.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Global from '/docs/cmd/_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# spo listitem attachment get

Gets an attachment from a list item

## Usage

```sh
m365 spo listitem attachment get [options]
```

## Options

```md definition-list
`-u, --webUrl <webUrl>`
: URL of the site where the list item is located.

`--listId [listId]`
: ID of the list. Specify either `listTitle`, `listId` or `listUrl`.

`--listTitle [listTitle]`
: Title of the list. Specify either `listTitle`, `listId` or `listUrl`.

`--listUrl [listUrl]`
: Server- or site-relative URL of the list. Specify either `listTitle`, `listId` or `listUrl`.

`--listItemId <listItemId>`
: The ID of the list item.

`-n, --fileName <fileName>`
: Name of the file to get.
```

<Global />

## Examples

Get an attachment from a list item by using list title.

```sh
m365 spo listitem attachment get --webUrl https://contoso.sharepoint.com/sites/project-x --listTitle "Demo List" --listItemId 147 --fileName "File1.jpg"
```

Get an attachment from a list item by using list URL.

```sh
m365 spo listitem attachment get --webUrl https://contoso.sharepoint.com/sites/project-x --listUrl "/sites/project-x/Lists/DemoList" --listItemId 147 --fileName "File1.jpg"
```

## Response

<Tabs>
<TabItem value="JSON">

```json
{
"FileName": "File1.jpg",
"FileNameAsPath": {
"DecodedUrl": "File1.jpg"
},
"ServerRelativePath": {
"DecodedUrl": "/sites/project-x/Lists/DemoListAttachments/147/File1.jpg"
},
"ServerRelativeUrl": "/sites/project-x/Lists/DemoListAttachments/147/File1.jpg"
}
```

</TabItem>
<TabItem value="Text">

```text
FileName : File1.jpg
FileNameAsPath : {"DecodedUrl":"File1.jpg"}
ServerRelativePath: {"DecodedUrl":"/sites/project-x/Lists/DemoListAttachments/147/File1.jpg"}
ServerRelativeUrl : /sites/project-x/Lists/DemoListAttachments/147/File1.jpg
```

</TabItem>
<TabItem value="CSV">

```csv
FileName,ServerRelativeUrl
File1.jpg,/sites/project-x/Lists/DemoListAttachments/147/File1.jpg
```

</TabItem>
<TabItem value="Markdown">

```md
# spo listitem attachment get --webUrl "https://contoso.sharepoint.com/sites/project-x" --listTitle "PnP PowerShell List" --listItemId "1" --fileName "File1.jpg"

Date: 7/20/2023

Property | Value
---------|-------
FileName | File1.jpg
ServerRelativeUrl | /sites/project-x/Lists/DemoListAttachments/147/File1.jpg
```

</TabItem>
</Tabs>
5 changes: 5 additions & 0 deletions docs/src/config/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -2621,6 +2621,11 @@ const sidebars = {
label: 'listitem remove',
id: 'cmd/spo/listitem/listitem-remove'
},
{
type: 'doc',
label: 'listitem attachment get',
id: 'cmd/spo/listitem/listitem-attachment-get'
},
{
type: 'doc',
label: 'listitem attachment list',
Expand Down
1 change: 1 addition & 0 deletions src/m365/spo/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export default {
LIST_WEBHOOK_REMOVE: `${prefix} list webhook remove`,
LIST_WEBHOOK_SET: `${prefix} list webhook set`,
LISTITEM_ADD: `${prefix} listitem add`,
LISTITEM_ATTACHMENT_GET: `${prefix} listitem attachment get`,
LISTITEM_ATTACHMENT_LIST: `${prefix} listitem attachment list`,
LISTITEM_BATCH_ADD: `${prefix} listitem batch add`,
LISTITEM_BATCH_SET: `${prefix} listitem batch set`,
Expand Down
209 changes: 209 additions & 0 deletions src/m365/spo/commands/listitem/listitem-attachment-get.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import assert from 'assert';
import sinon from 'sinon';
import { telemetry } from '../../../../telemetry.js';
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 request from '../../../../request.js';
import { formatting } from '../../../../utils/formatting.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import { urlUtil } from '../../../../utils/urlUtil.js';
import commands from '../../commands.js';
import command from './listitem-attachment-get.js';

describe(commands.LISTITEM_ATTACHMENT_GET, () => {
const webUrl = 'https://contoso.sharepoint.com/sites/project-x';
const listId = '4fc5ba1e-18b7-49e0-81fe-54515cc2eede';
const listTitle = 'Demo List';
const listUrl = '/sites/project-x/Lists/DemoList';
const listItemId = 147;
const fileName = 'File1.jpg';
const listServerRelativeUrl: string = urlUtil.getServerRelativePath(webUrl, listUrl);

let cli: Cli;
let log: any[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;

const attachmentResponse = {
"FileName": "File1.jpg",
"FileNameAsPath": {
"DecodedUrl": "File1.jpg"
},
"ServerRelativePath": {
"DecodedUrl": "/sites/project-x/Lists/DemoListAttachments/147/File1.jpg"
},
"ServerRelativeUrl": "/sites/project-x/Lists/DemoListAttachments/147/File1.jpg"
};

const getFakes = async (opts: any) => {
if ((opts.url as string).indexOf('/_api/web/lists') > -1) {
return attachmentResponse;
}

if (opts.url === `https://contoso.sharepoint.com/sites/project-x/_api/web/GetList('${formatting.encodeQueryParameter(listServerRelativeUrl)}')/items(147)/AttachmentFiles('${fileName}')`) {
return attachmentResponse;
}

throw 'Invalid request';
};

before(() => {
cli = Cli.getInstance();
sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve());
sinon.stub(telemetry, 'trackEvent').callsFake(() => { });
sinon.stub(pid, 'getProcessName').callsFake(() => '');
sinon.stub(session, 'getId').callsFake(() => '');
nanddeepn marked this conversation as resolved.
Show resolved Hide resolved
auth.service.connected = true;
commandInfo = Cli.getCommandInfo(command);
});

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
loggerLogSpy = sinon.spy(logger, 'log');
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake(((settingName, defaultValue) => defaultValue));
});

afterEach(() => {
sinonUtil.restore([
request.get,
cli.getSettingWithDefaultValue
]);
});

after(() => {
sinon.restore();
auth.service.connected = false;
});

it('has correct name', () => {
assert.strictEqual(command.name, commands.LISTITEM_ATTACHMENT_GET);
});

it('has a description', () => {
assert.notStrictEqual(command.description, null);
});

it('supports specifying URL', () => {
const options = command.options;
let containsTypeOption = false;
options.forEach(o => {
if (o.option.indexOf('<webUrl>') > -1) {
containsTypeOption = true;
}
});
assert(containsTypeOption);
});

it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => {
const actual = await command.validate({ options: { webUrl: 'foo', listTitle: 'Demo List', listItemId: listItemId, fileName: fileName } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('passes validation if the webUrl option is a valid SharePoint site URL', async () => {
const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', listTitle: 'Demo List', listItemId: listItemId, fileName: fileName } }, commandInfo);
assert(actual);
});

it('fails validation if the listId option is not a valid GUID', async () => {
const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', listId: 'foo', listItemId: listItemId, fileName: fileName } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('passes validation if the listId option is a valid GUID', async () => {
const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', listId: listId, listItemId: listItemId, fileName: fileName } }, commandInfo);
assert(actual);
});

it('fails validation if the specified listItemId is not a number', async () => {
const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', listTitle: 'Demo List', listItemId: 'a', fileName: fileName } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('returns attachment from a list item by listId', async () => {
sinon.stub(request, 'get').callsFake(getFakes);

const options: any = {
debug: true,
webUrl: webUrl,
listId: listId,
listItemId: listItemId,
fileName: fileName
};

await command.action(logger, { options: options } as any);
assert(loggerLogSpy.calledWith(attachmentResponse));
nanddeepn marked this conversation as resolved.
Show resolved Hide resolved
});

it('returns attachment from a list item by listTitle', async () => {
sinon.stub(request, 'get').callsFake(getFakes);

const options: any = {
debug: true,
webUrl: webUrl,
listTitle: listTitle,
listItemId: listItemId,
fileName: fileName
};

await command.action(logger, { options: options } as any);
assert(loggerLogSpy.calledWith(attachmentResponse));
nanddeepn marked this conversation as resolved.
Show resolved Hide resolved
});

it('returns attachment from a list item by listUrl', async () => {
sinon.stub(request, 'get').callsFake(getFakes);

const options: any = {
debug: true,
webUrl: webUrl,
listUrl: listUrl,
listItemId: listItemId,
fileName: fileName
};

await command.action(logger, { options: options } as any);
assert(loggerLogSpy.calledWith(attachmentResponse));
});

it('correctly handles random API error', async () => {
sinon.stub(request, 'get').callsFake(() => Promise.reject('An error has occurred'));
nanddeepn marked this conversation as resolved.
Show resolved Hide resolved

const options: any = {
webUrl: webUrl,
listId: listId,
listItemId: listItemId,
fileName: fileName
};

await assert.rejects(command.action(logger, { options: options } as any), new CommandError('An error has occurred'));
});

it('correctly handles no attachment found', async () => {
sinon.stub(request, 'get').rejects(new Error('Specified argument was out of the range of valid values.\r\nParameter name: fileName'));

await assert.rejects(command.action(logger, {
options: {
webUrl: webUrl,
listId: listId,
listItemId: listItemId,
fileName: fileName
}
} as any), new CommandError('Specified argument was out of the range of valid values.\r\nParameter name: fileName'));
});
});
Loading