-
Notifications
You must be signed in to change notification settings - Fork 328
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 command listitem attachment add
#5345
Closed
MathijsVerbeeck
wants to merge
3
commits into
pnp:main
from
MathijsVerbeeck:spo-listitem-attachment-add
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import Global from '/docs/cmd/_global.mdx'; | ||
import Tabs from '@theme/Tabs'; | ||
import TabItem from '@theme/TabItem'; | ||
|
||
# spo listitem attachment add | ||
|
||
Adds an attachment to a list item | ||
|
||
## Usage | ||
|
||
```sh | ||
m365 spo listitem attachment add [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. | ||
|
||
`-p, --filePath <filePath>` | ||
: Local path to the file that will be added as an attachment to the list item. | ||
|
||
`-n, --fileName [fileName]` | ||
: Name for the file. If no name is provided, the name from `filePath` will be utilized. | ||
``` | ||
|
||
<Global /> | ||
|
||
## Examples | ||
|
||
Add a new attachment to a list item from a local file by using list title | ||
|
||
```sh | ||
m365 spo listitem attachment add --webUrl https://contoso.sharepoint.com/sites/project-x --listTitle "DemoList" --listItemId 147 --filePath "C:/Reports/File1.jpg" | ||
``` | ||
|
||
Add a new attachment to a list item from a local file by using list URL with a different filename | ||
|
||
```sh | ||
m365 spo listitem attachment add --webUrl https://contoso.sharepoint.com/sites/project-x --listUrl "/sites/project-x/Lists/DemoList" --listItemId 147 --filePath "C:/Reports/File1.jpg" --fileName "File2.jpg" | ||
``` | ||
|
||
## Response | ||
|
||
<Tabs> | ||
<TabItem value="JSON"> | ||
|
||
```json | ||
[ | ||
{ | ||
"FileName": "File1.jpg", | ||
"FileNameAsPath": { | ||
"DecodedUrl": "File1.jpg" | ||
}, | ||
"ServerRelativePath": { | ||
"DecodedUrl": "/Lists/DemoList/Attachments/147/File1.jpg" | ||
}, | ||
"ServerRelativeUrl": "/Lists/Test/Attachments/147/File1.jpg" | ||
} | ||
] | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="Text"> | ||
|
||
```text | ||
FileName : File1.jpg | ||
FileNameAsPath : {"DecodedUrl":"File1jpg"} | ||
ServerRelativePath: {"DecodedUrl":"/Lists/DemoList/Attachments/743/File1.jpg"} | ||
ServerRelativeUrl : /Lists/DemoList/Attachments/147/File1.jpg | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="CSV"> | ||
|
||
```csv | ||
FileName,ServerRelativeUrl | ||
File1.jpg,/Lists/DemoList/Attachments/147/File1.jpg | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="Markdown"> | ||
|
||
```md | ||
# spo listitem attachment add --webUrl https://contoso.sharepoint.com/sites/project-x --listTitle "DemoList" --listItemId 147 --filePath "C:/Reports/File1.jpg" | ||
|
||
Date: 25/07/2023 | ||
|
||
Property | Value | ||
---------|------- | ||
FileName | File1.jpg | ||
ServerRelativeUrl | /Lists/DemoList/Attachments/147/File1.jpg | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
220 changes: 220 additions & 0 deletions
220
src/m365/spo/commands/listitem/listitem-attachment-add.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
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 { 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 fs from 'fs'; | ||
import request from '../../../../request.js'; | ||
import command from './listitem-attachment-add.js'; | ||
|
||
describe(commands.LISTITEM_ATTACHMENT_ADD, () => { | ||
const webUrl = 'https://contoso.sharepoint.com/sites/project-x'; | ||
const listId = '236a0f92482d475bba8fd0e4f78555e4'; | ||
const listTitle = 'Test list'; | ||
const listUrl = 'sites/project-x/lists/testlist'; | ||
const listServerRelativeUrl: string = urlUtil.getServerRelativePath(webUrl, listUrl); | ||
const listItemId = 1; | ||
const filePath = 'C:\\Temp\\Test.pdf'; | ||
const fileName = 'CLIRocks.pdf'; | ||
|
||
const response = { 'FileName': 'CLIRocks.pdf', 'FileNameAsPath': { 'DecodedUrl': 'Testje.pdf' }, 'ServerRelativePath': { 'DecodedUrl': '/Lists/aaaaaa/Attachments/743/Testje.pdf' }, 'ServerRelativeUrl': '/Lists/aaaaaa/Attachments/743/Testje.pdf' }; | ||
|
||
let cli: Cli; | ||
let log: any[]; | ||
let logger: Logger; | ||
let loggerLogSpy: sinon.SinonSpy; | ||
let commandInfo: CommandInfo; | ||
|
||
before(() => { | ||
cli = Cli.getInstance(); | ||
sinon.stub(auth, 'restoreAuth').resolves(); | ||
sinon.stub(telemetry, 'trackEvent').returns(); | ||
sinon.stub(pid, 'getProcessName').returns(''); | ||
sinon.stub(session, 'getId').returns(''); | ||
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(((_, defaultValue) => defaultValue)); | ||
}); | ||
|
||
afterEach(() => { | ||
sinonUtil.restore([, | ||
fs.existsSync, | ||
fs.readFileSync, | ||
request.post, | ||
cli.getSettingWithDefaultValue | ||
]); | ||
}); | ||
|
||
after(() => { | ||
sinon.restore(); | ||
auth.service.connected = false; | ||
}); | ||
|
||
it('has correct name', () => { | ||
assert.strictEqual(command.name, commands.LISTITEM_ATTACHMENT_ADD); | ||
}); | ||
|
||
it('has a description', () => { | ||
assert.notStrictEqual(command.description, null); | ||
}); | ||
|
||
it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => { | ||
sinon.stub(fs, 'existsSync').returns(true); | ||
const actual = await command.validate({ options: { webUrl: 'invalid', listTitle: listTitle, listItemId: listItemId, filePath: filePath } }, commandInfo); | ||
assert.notStrictEqual(actual, true); | ||
}); | ||
|
||
it('passes validation if the webUrl option is a valid SharePoint site URL and filePath exists', async () => { | ||
sinon.stub(fs, 'existsSync').returns(true); | ||
const actual = await command.validate({ options: { webUrl: webUrl, listTitle: listTitle, listItemId: listItemId, filePath: filePath } }, commandInfo); | ||
assert(actual); | ||
}); | ||
|
||
it('fails validation if the listId option is not a valid GUID', async () => { | ||
sinon.stub(fs, 'existsSync').returns(true); | ||
const actual = await command.validate({ options: { webUrl: webUrl, listId: 'invalid', listItemId: listItemId, filePath: filePath } }, commandInfo); | ||
assert.notStrictEqual(actual, true); | ||
}); | ||
|
||
it('fails validation if the listItemId option is not a valid number', async () => { | ||
sinon.stub(fs, 'existsSync').returns(true); | ||
const actual = await command.validate({ options: { webUrl: webUrl, listId: listId, listItemId: 'invalid', filePath: filePath } }, commandInfo); | ||
assert.notStrictEqual(actual, true); | ||
}); | ||
|
||
it('passes validation if the listId option is a valid GUID', async () => { | ||
sinon.stub(fs, 'existsSync').returns(true); | ||
const actual = await command.validate({ options: { webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath } }, commandInfo); | ||
assert(actual); | ||
}); | ||
|
||
it('fails validation if filePath does not exist', async () => { | ||
sinon.stub(fs, 'existsSync').returns(false); | ||
const actual = await command.validate({ options: { webUrl: webUrl, listTitle: listTitle, listItemId: listItemId, filePath: filePath } }, commandInfo); | ||
assert.notStrictEqual(actual, true); | ||
}); | ||
|
||
it('adds attachment to listitem in list retrieved by id while specifying fileName', async () => { | ||
sinon.stub(fs, 'existsSync').returns(true); | ||
sinon.stub(fs, 'readFileSync').returns('content read'); | ||
sinon.stub(request, 'post').callsFake(async (args) => { | ||
if (args.url === `${webUrl}/_api/web/lists(guid'${listId}')/items(${listItemId})/AttachmentFiles/add(FileName='${fileName}')`) { | ||
return response; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
await command.action(logger, { options: { verbose: true, webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath, fileName: fileName } }); | ||
assert(loggerLogSpy.calledOnceWith(response)); | ||
}); | ||
|
||
it('adds attachment to listitem in list retrieved by url while not specifying fileName', async () => { | ||
sinon.stub(fs, 'existsSync').returns(true); | ||
sinon.stub(fs, 'readFileSync').returns('content read'); | ||
sinon.stub(request, 'post').callsFake(async (args) => { | ||
if (args.url === `${webUrl}/_api/web/GetList('${formatting.encodeQueryParameter(listServerRelativeUrl)}')/items(${listItemId})/AttachmentFiles/add(FileName='${filePath.replace(/^.*[\\\/]/, '')}')`) { | ||
return response; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
await command.action(logger, { options: { verbose: true, webUrl: webUrl, listUrl: listUrl, listItemId: listItemId, filePath: filePath } }); | ||
assert(loggerLogSpy.calledOnceWith(response)); | ||
}); | ||
|
||
it('adds attachment to listitem in list retrieved by url while specifying fileName without extension', async () => { | ||
sinon.stub(fs, 'existsSync').returns(true); | ||
sinon.stub(fs, 'readFileSync').returns('content read'); | ||
const fileNameWithoutExtension = fileName.split('.')[0]; | ||
const fileNameWithExtension = `${fileNameWithoutExtension}.${filePath.split('.').pop()}`; | ||
sinon.stub(request, 'post').callsFake(async (args) => { | ||
if (args.url === `${webUrl}/_api/web/lists/getByTitle('${formatting.encodeQueryParameter(listTitle)}')/items(${listItemId})/AttachmentFiles/add(FileName='${fileNameWithExtension}')`) { | ||
return response; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
await command.action(logger, { options: { verbose: true, webUrl: webUrl, listTitle: listTitle, listItemId: listItemId, filePath: filePath, fileName: fileNameWithoutExtension } }); | ||
assert(loggerLogSpy.calledOnceWith(response)); | ||
}); | ||
|
||
it('handles error when file with specific name already exists', async () => { | ||
sinon.stub(fs, 'existsSync').returns(true); | ||
sinon.stub(fs, 'readFileSync').returns('content read'); | ||
const error = { | ||
error: { | ||
'odata.error': { | ||
code: '-2130575257, Microsoft.SharePoint.SPException', | ||
message: { | ||
lang: 'en-US', | ||
value: 'The specified name is already in use.\n\nThe document or folder name was not changed. To change the name to a different value, close this dialog and edit the properties of the document or folder.' | ||
} | ||
} | ||
} | ||
}; | ||
sinon.stub(request, 'post').callsFake(async (args) => { | ||
if (args.url === `${webUrl}/_api/web/lists(guid'${listId}')/items(${listItemId})/AttachmentFiles/add(FileName='${fileName}')`) { | ||
throw error; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
await assert.rejects(command.action(logger, { options: { verbose: true, webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath, fileName: fileName } }), | ||
new CommandError(error.error['odata.error'].message.value.split('\n')[0])); | ||
}); | ||
|
||
it('handles API error', async () => { | ||
sinon.stub(fs, 'existsSync').returns(true); | ||
sinon.stub(fs, 'readFileSync').returns('content read'); | ||
const error = { | ||
error: { | ||
'odata.error': { | ||
code: '-2130575257, Microsoft.SharePoint.SPException', | ||
message: { | ||
lang: 'en-US', | ||
value: 'An error has occured.' | ||
} | ||
} | ||
} | ||
}; | ||
sinon.stub(request, 'post').callsFake(async (args) => { | ||
if (args.url === `${webUrl}/_api/web/lists(guid'${listId}')/items(${listItemId})/AttachmentFiles/add(FileName='${fileName}')`) { | ||
throw error; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
await assert.rejects(command.action(logger, { options: { verbose: true, webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath, fileName: fileName } }), | ||
new CommandError(error.error['odata.error'].message.value)); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For
fileName
, we don't need to define the extension as this is done in code.