Skip to content

Commit 3406d28

Browse files
nicodecleyreplamber
authored andcommitted
Adds 'spo listitem record unlock' command. Closes pnp#4282
1 parent 69e7422 commit 3406d28

File tree

5 files changed

+418
-2
lines changed

5 files changed

+418
-2
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# spo listitem record unlock
2+
3+
Unlocks the list item record
4+
5+
## Usage
6+
7+
```sh
8+
m365 spo listitem record unlock [options]
9+
```
10+
11+
## Options
12+
13+
`-u, --webUrl <webUrl>`
14+
: The url of the web
15+
16+
`--listItemId <listItemId>`
17+
: ID of the list item that should be unlocked
18+
19+
`--listId [listId]`
20+
: ID of the list. Specify either `listTitle`, `listId` or `listUrl`
21+
22+
`--listTitle [listTitle]`
23+
: Title of the list. Specify either `listTitle`, `listId` or `listUrl`
24+
25+
`--listUrl [listUrl]`
26+
: Server- or site-relative URL of the list. Specify either `listTitle`, `listId` or `listUrl`
27+
28+
--8<-- "docs/cmd/_global.md"
29+
30+
## Examples
31+
32+
Unlocks the list item record in a given site based on the list id
33+
34+
```sh
35+
m365 spo listitem record unlock --webUrl https://contoso.sharepoint.com/sites/project-x --listId 0cd891ef-afce-4e55-b836-fce03286cccf --listItemId 1
36+
```
37+
38+
Unlocks the list item record in a given site based on the list title
39+
40+
```sh
41+
m365 spo listitem record unlock --webUrl https://contoso.sharepoint.com/sites/project-x --listTitle 'List 1' --listItemId 1
42+
```
43+
44+
Unlocks the list item record in a given site based on the server relative list url
45+
46+
```sh
47+
m365 spo listitem record unlock --webUrl https://contoso.sharepoint.com/sites/project-x --listUrl /sites/project-x/lists/TestList --listItemId 1
48+
```
49+
50+
## Response
51+
52+
The command won't return a response on success.

docs/mkdocs.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,14 +485,15 @@ nav:
485485
- listitem remove: cmd/spo/listitem/listitem-remove.md
486486
- listitem attachment list: cmd/spo/listitem/listitem-attachment-list.md
487487
- listitem batch add: cmd/spo/listitem/listitem-batch-add.md
488-
- listitem record lock: cmd/spo/listitem/listitem-record-lock.md
489488
- listitem retentionlabel remove: cmd/spo/listitem/listitem-retentionlabel-remove.md
490489
- listitem roleassignment add: cmd/spo/listitem/listitem-roleassignment-add.md
491490
- listitem roleassignment remove: cmd/spo/listitem/listitem-roleassignment-remove.md
492491
- listitem roleinheritance break: cmd/spo/listitem/listitem-roleinheritance-break.md
493492
- listitem roleinheritance reset: cmd/spo/listitem/listitem-roleinheritance-reset.md
494493
- listitem record declare: cmd/spo/listitem/listitem-record-declare.md
494+
- listitem record lock: cmd/spo/listitem/listitem-record-lock.md
495495
- listitem record undeclare: cmd/spo/listitem/listitem-record-undeclare.md
496+
- listitem record unlock: cmd/spo/listitem/listitem-record-unlock.md
496497
- mail:
497498
- mail send: cmd/spo/mail/mail-send.md
498499
- navigation:

src/m365/spo/commands.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,10 @@ export default {
142142
LISTITEM_ISRECORD: `${prefix} listitem isrecord`,
143143
LISTITEM_LIST: `${prefix} listitem list`,
144144
LISTITEM_RECORD_DECLARE: `${prefix} listitem record declare`,
145+
LISTITEM_RECORD_LOCK: `${prefix} listitem record lock`,
145146
LISTITEM_RECORD_UNDECLARE: `${prefix} listitem record undeclare`,
147+
LISTITEM_RECORD_UNLOCK: `${prefix} listitem record unlock`,
146148
LISTITEM_REMOVE: `${prefix} listitem remove`,
147-
LISTITEM_RECORD_LOCK: `${prefix} listitem record lock`,
148149
LISTITEM_RETENTIONLABEL_REMOVE: `${prefix} listitem retentionlabel remove`,
149150
LISTITEM_ROLEASSIGNMENT_ADD: `${prefix} listitem roleassignment add`,
150151
LISTITEM_ROLEASSIGNMENT_REMOVE: `${prefix} listitem roleassignment remove`,
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import * as assert from 'assert';
2+
import * as sinon from 'sinon';
3+
import appInsights from '../../../../appInsights';
4+
import auth from '../../../../Auth';
5+
import { Cli } from '../../../../cli/Cli';
6+
import { CommandInfo } from '../../../../cli/CommandInfo';
7+
import { Logger } from '../../../../cli/Logger';
8+
import Command, { CommandError } from '../../../../Command';
9+
import request from '../../../../request';
10+
import { pid } from '../../../../utils/pid';
11+
import { sinonUtil } from '../../../../utils/sinonUtil';
12+
import commands from '../../commands';
13+
const command: Command = require('./listitem-record-unlock');
14+
15+
describe(commands.LISTITEM_RECORD_UNLOCK, () => {
16+
let log: any[];
17+
let logger: Logger;
18+
let commandInfo: CommandInfo;
19+
const listUrl = "/MyLibrary";
20+
const listTitle = "MyLibrary";
21+
const listId = "cc27a922-8224-4296-90a5-ebbc54da2e85";
22+
const webUrl = "https://contoso.sharepoint.com";
23+
const listResponse = {
24+
"RootFolder": {
25+
"Exists": true,
26+
"IsWOPIEnabled": false,
27+
"ItemCount": 0,
28+
"Name": listTitle,
29+
"ProgID": null,
30+
"ServerRelativeUrl": listUrl,
31+
"TimeCreated": "2019-01-11T10:03:19Z",
32+
"TimeLastModified": "2019-01-11T10:03:20Z",
33+
"UniqueId": listId,
34+
"WelcomePage": ""
35+
}
36+
};
37+
38+
before(() => {
39+
sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve());
40+
sinon.stub(appInsights, 'trackEvent').callsFake(() => { });
41+
auth.service.connected = true;
42+
commandInfo = Cli.getCommandInfo(command);
43+
});
44+
45+
beforeEach(() => {
46+
log = [];
47+
logger = {
48+
log: (msg: string) => {
49+
log.push(msg);
50+
},
51+
logRaw: (msg: string) => {
52+
log.push(msg);
53+
},
54+
logToStderr: (msg: string) => {
55+
log.push(msg);
56+
}
57+
};
58+
});
59+
60+
afterEach(() => {
61+
sinonUtil.restore([
62+
request.get,
63+
request.post
64+
]);
65+
});
66+
67+
after(() => {
68+
sinonUtil.restore([
69+
auth.restoreAuth,
70+
appInsights.trackEvent,
71+
pid.getProcessName
72+
]);
73+
auth.service.connected = false;
74+
});
75+
76+
it('has correct name', () => {
77+
assert.strictEqual(command.name, commands.LISTITEM_RECORD_UNLOCK);
78+
});
79+
80+
it('has a description', () => {
81+
assert.notStrictEqual(command.description, null);
82+
});
83+
84+
it('unlocks a list item based on listUrl (debug)', async () => {
85+
sinon.stub(request, 'post').callsFake(async (opts) => {
86+
if (opts.url === `https://contoso.sharepoint.com/_api/SP.CompliancePolicy.SPPolicyStoreProxy.UnlockRecordItem()`) {
87+
return;
88+
}
89+
90+
throw 'Invalid request';
91+
});
92+
93+
await assert.doesNotReject(command.action(logger, {
94+
options: {
95+
verbose: true,
96+
listUrl: listUrl,
97+
webUrl: webUrl,
98+
listItemId: 1
99+
}
100+
}));
101+
});
102+
103+
it('unlocks a list item based on listTitle', async () => {
104+
sinon.stub(request, 'get').callsFake(async (opts) => {
105+
if (opts.url === `https://contoso.sharepoint.com/_api/web/lists/getByTitle('${listTitle}')/?$expand=RootFolder&$select=RootFolder`) {
106+
return listResponse;
107+
}
108+
109+
throw 'Invalid request';
110+
});
111+
112+
sinon.stub(request, 'post').callsFake(async (opts) => {
113+
if (opts.url === `https://contoso.sharepoint.com/_api/SP.CompliancePolicy.SPPolicyStoreProxy.UnlockRecordItem()`) {
114+
return;
115+
}
116+
117+
throw 'Invalid request';
118+
});
119+
120+
await assert.doesNotReject(command.action(logger, {
121+
options: {
122+
listTitle: listTitle,
123+
webUrl: webUrl,
124+
listItemId: 1
125+
}
126+
}));
127+
});
128+
129+
it('unlocks a list item based on listId', async () => {
130+
sinon.stub(request, 'get').callsFake(async (opts) => {
131+
if (opts.url === `https://contoso.sharepoint.com/_api/web/lists(guid'${listId}')/?$expand=RootFolder&$select=RootFolder`) {
132+
return listResponse;
133+
}
134+
135+
throw 'Invalid request';
136+
});
137+
138+
sinon.stub(request, 'post').callsFake(async (opts) => {
139+
if (opts.url === `https://contoso.sharepoint.com/_api/SP.CompliancePolicy.SPPolicyStoreProxy.UnlockRecordItem()`) {
140+
return;
141+
}
142+
143+
throw 'Invalid request';
144+
});
145+
146+
await assert.doesNotReject(command.action(logger, {
147+
options: {
148+
listId: listId,
149+
webUrl: webUrl,
150+
listItemId: 1
151+
}
152+
}));
153+
});
154+
155+
it('correctly handles API OData error', async () => {
156+
const errorMessage = 'Something went wrong';
157+
158+
sinon.stub(request, 'post').callsFake(async () => { throw { error: { error: { message: errorMessage } } }; });
159+
160+
await assert.rejects(command.action(logger, {
161+
options: {
162+
listUrl: listUrl,
163+
webUrl: webUrl,
164+
listItemId: 1
165+
}
166+
}), new CommandError(errorMessage));
167+
});
168+
169+
it('fails validation if both id and title options are not passed', async () => {
170+
const actual = await command.validate({ options: { webUrl: webUrl, listItemId: 1 } }, commandInfo);
171+
assert.notStrictEqual(actual, true);
172+
});
173+
174+
it('fails validation if the url option is not a valid SharePoint site URL', async () => {
175+
const actual = await command.validate({ options: { webUrl: 'foo', listItemId: 1, listTitle: listTitle } }, commandInfo);
176+
assert.notStrictEqual(actual, true);
177+
});
178+
179+
it('passes validation if the url option is a valid SharePoint site URL', async () => {
180+
const actual = await command.validate({ options: { webUrl: webUrl, listId: listId, listItemId: 1 } }, commandInfo);
181+
assert(actual);
182+
});
183+
184+
it('fails validation if the id option is not a valid GUID', async () => {
185+
const actual = await command.validate({ options: { webUrl: webUrl, listId: '12345', listItemId: 1 } }, commandInfo);
186+
assert.notStrictEqual(actual, true);
187+
});
188+
189+
it('passes validation if the id option is a valid GUID', async () => {
190+
const actual = await command.validate({ options: { webUrl: webUrl, listId: listId, listItemId: 1 } }, commandInfo);
191+
assert(actual);
192+
});
193+
194+
it('fails validation if both id and title options are passed', async () => {
195+
const actual = await command.validate({ options: { webUrl: webUrl, listId: listId, listTitle: listTitle, listItemId: 1 } }, commandInfo);
196+
assert.notStrictEqual(actual, true);
197+
});
198+
199+
it('fails validation if id is not passed', async () => {
200+
const actual = await command.validate({ options: { webUrl: webUrl } }, commandInfo);
201+
assert.notStrictEqual(actual, true);
202+
});
203+
204+
it('fails validation if id is not a number', async () => {
205+
const actual = await command.validate({ options: { webUrl: webUrl, listItemId: 'abc', listTitle: listTitle } }, commandInfo);
206+
assert.notStrictEqual(actual, true);
207+
});
208+
});

0 commit comments

Comments
 (0)