Skip to content

Commit 6686856

Browse files
Adds spo site alert get command. Closes #6654
1 parent dce6352 commit 6686856

File tree

7 files changed

+1002
-4
lines changed

7 files changed

+1002
-4
lines changed

.devproxy/api-specs/sharepoint-admin.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,46 @@ paths:
6262
responses:
6363
200:
6464
description: OK
65+
/_api/SPO.Tenant/sites('{id}'):
66+
get:
67+
parameters:
68+
- name: id
69+
in: path
70+
required: true
71+
schema:
72+
type: string
73+
description: The site collection ID (GUID)
74+
- name: $select
75+
in: query
76+
required: false
77+
schema:
78+
type: string
79+
description: e.g. "$select=Url" to retrieve only the site URL
80+
security:
81+
- delegated:
82+
- AllSites.FullControl
83+
- application:
84+
- Sites.FullControl.All
85+
responses:
86+
200:
87+
description: OK
88+
/_api/web/lists/GetByTitle('{listTitle}')/RenderListDataAsStream:
89+
post:
90+
parameters:
91+
- name: listTitle
92+
in: path
93+
required: true
94+
schema:
95+
type: string
96+
description: The list title (e.g. DO_NOT_DELETE_SPLIST_TENANTADMIN_AGGREGATED_SITECOLLECTIONS)
97+
security:
98+
- delegated:
99+
- AllSites.FullControl
100+
- application:
101+
- Sites.FullControl.All
102+
responses:
103+
200:
104+
description: OK
65105
x-ms-generated-by:
66106
toolName: Dev Proxy
67107
toolVersion: 0.25.0

docs/docs/cmd/spo/tenant/tenant-site-get.mdx

Lines changed: 479 additions & 0 deletions
Large diffs are not rendered by default.

docs/src/config/sidebars.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4029,6 +4029,11 @@ const sidebars: SidebarsConfig = {
40294029
type: 'doc',
40304030
label: 'tenant settings set',
40314031
id: 'cmd/spo/tenant/tenant-settings-set'
4032+
},
4033+
{
4034+
type: 'doc',
4035+
label: 'tenant site get',
4036+
id: 'cmd/spo/tenant/tenant-site-get'
40324037
}
40334038
]
40344039
},

src/m365/spo/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ export default {
336336
TENANT_RECYCLEBINITEM_RESTORE: `${prefix} tenant recyclebinitem restore`,
337337
TENANT_SETTINGS_LIST: `${prefix} tenant settings list`,
338338
TENANT_SETTINGS_SET: `${prefix} tenant settings set`,
339+
TENANT_SITE_GET: `${prefix} tenant site get`,
339340
TERM_ADD: `${prefix} term add`,
340341
TERM_GET: `${prefix} term get`,
341342
TERM_LIST: `${prefix} term list`,
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import assert from 'assert';
2+
import sinon from 'sinon';
3+
import auth from '../../../../Auth.js';
4+
import { cli } from '../../../../cli/cli.js';
5+
import { CommandInfo } from '../../../../cli/CommandInfo.js';
6+
import { Logger } from '../../../../cli/Logger.js';
7+
import { CommandError } from '../../../../Command.js';
8+
import request from '../../../../request.js';
9+
import { telemetry } from '../../../../telemetry.js';
10+
import { pid } from '../../../../utils/pid.js';
11+
import { session } from '../../../../utils/session.js';
12+
import { sinonUtil } from '../../../../utils/sinonUtil.js';
13+
import commands from '../../commands.js';
14+
import command from './tenant-site-get.js';
15+
import { spo, TenantSiteProperties } from '../../../../utils/spo.js';
16+
import { settingsNames } from '../../../../settingsNames.js';
17+
18+
describe(commands.TENANT_SITE_GET, () => {
19+
const adminUrl = 'https://contoso-admin.sharepoint.com';
20+
const siteId = '3ae83bc5-1f27-45c1-9eee-1bd1e2ddce69';
21+
const siteUrl = 'https://contoso.sharepoint.com/sites/Marketing';
22+
23+
let log: any[];
24+
let logger: Logger;
25+
let loggerLogSpy: sinon.SinonSpy;
26+
let commandInfo: CommandInfo;
27+
28+
const siteResponse: TenantSiteProperties = {
29+
AllowDownloadingNonWebViewableFiles: false,
30+
AllowEditing: true,
31+
AllowFileArchive: false,
32+
AllowSelfServiceUpgrade: true,
33+
AllowWebPropertyBagUpdateWhenDenyAddAndCustomizePagesIsEnabled: false,
34+
AnonymousLinkExpirationInDays: 0,
35+
ApplyToExistingDocumentLibraries: false,
36+
ApplyToNewDocumentLibraries: false,
37+
ArchivedBy: "",
38+
ArchivedFileDiskUsed: "0",
39+
ArchivedTime: "0001-01-01T00:00:00",
40+
ArchiveStatus: "NotArchived",
41+
AuthContextStrength: null,
42+
AuthenticationContextLimitedAccess: false,
43+
AuthenticationContextName: null,
44+
AverageResourceUsage: 0,
45+
BlockDownloadLinksFileType: 1,
46+
BlockDownloadMicrosoft365GroupIds: null,
47+
BlockDownloadPolicy: false,
48+
BlockDownloadPolicyFileTypeIds: null,
49+
BlockGuestsAsSiteAdmin: 0,
50+
BonusDiskQuota: "920",
51+
ClearGroupId: false,
52+
ClearRestrictedAccessControl: false,
53+
CommentsOnSitePagesDisabled: false,
54+
CompatibilityLevel: 15,
55+
ConditionalAccessPolicy: 0,
56+
CreatedTime: "2021-10-12T09:54:16.52",
57+
CurrentResourceUsage: 0,
58+
DefaultLinkPermission: 0,
59+
DefaultLinkToExistingAccess: false,
60+
DefaultLinkToExistingAccessReset: false,
61+
DefaultShareLinkRole: 0,
62+
DefaultShareLinkScope: -1,
63+
DefaultSharingLinkType: 0,
64+
DenyAddAndCustomizePages: 2,
65+
Description: "",
66+
DisableAppViews: 2,
67+
DisableCompanyWideSharingLinks: 2,
68+
DisableFlows: 2,
69+
DisableSiteBranding: false,
70+
EnableAutoExpirationVersionTrim: false,
71+
ExcludeBlockDownloadPolicySiteOwners: false,
72+
ExcludeBlockDownloadSharePointGroups: [],
73+
ExcludedBlockDownloadGroupIds: [],
74+
ExpireVersionsAfterDays: 0,
75+
ExternalUserExpirationInDays: 0,
76+
FileTypesForVersionExpiration: null,
77+
GroupId: "00000000-0000-0000-0000-000000000000",
78+
GroupOwnerLoginName: "c:0o.c|federateddirectoryclaimprovider|00000000-0000-0000-0000-000000000000_o",
79+
HasHolds: false,
80+
HidePeoplePreviewingFiles: false,
81+
HidePeopleWhoHaveListsOpen: false,
82+
HubSiteId: "af80c11f-0138-4d72-bb37-514542c3aabb",
83+
IBMode: "",
84+
IBSegments: [],
85+
IBSegmentsToAdd: null,
86+
IBSegmentsToRemove: null,
87+
InheritVersionPolicyFromTenant: true,
88+
IsAuthoritative: false,
89+
IsGroupOwnerSiteAdmin: false,
90+
IsHubSite: false,
91+
IsTeamsChannelConnected: false,
92+
IsTeamsConnected: false,
93+
LastContentModifiedDate: "2025-10-03T00:20:28.62",
94+
Lcid: "1033",
95+
LimitedAccessFileType: 1,
96+
ListsShowHeaderAndNavigation: false,
97+
LockIssue: null,
98+
LockReason: 0,
99+
LockState: "Unlock",
100+
LoopDefaultSharingLinkRole: 0,
101+
LoopDefaultSharingLinkScope: -1,
102+
MajorVersionLimit: 0,
103+
MajorWithMinorVersionsLimit: 0,
104+
MediaTranscription: 0,
105+
OverrideBlockUserInfoVisibility: 0,
106+
OverrideSharingCapability: false,
107+
OverrideTenantAnonymousLinkExpirationPolicy: false,
108+
OverrideTenantExternalUserExpirationPolicy: false,
109+
110+
OwnerEmail: "[email protected]",
111+
OwnerLoginName: "i:0#.f|membership|[email protected]",
112+
OwnerName: "john",
113+
PWAEnabled: 1,
114+
ReadOnlyAccessPolicy: false,
115+
ReadOnlyForBlockDownloadPolicy: false,
116+
ReadOnlyForUnmanagedDevices: false,
117+
RelatedGroupId: "00000000-0000-0000-0000-000000000000",
118+
RemoveVersionExpirationFileTypeOverride: null,
119+
RequestFilesLinkEnabled: false,
120+
RequestFilesLinkExpirationInDays: -1,
121+
RestrictContentOrgWideSearch: false,
122+
RestrictedAccessControl: false,
123+
RestrictedAccessControlGroups: [],
124+
RestrictedAccessControlGroupsToAdd: null,
125+
RestrictedAccessControlGroupsToRemove: null,
126+
RestrictedContentDiscoveryforCopilotAndAgents: false,
127+
RestrictedToRegion: 3,
128+
SandboxedCodeActivationCapability: 2,
129+
SensitivityLabel: "00000000-0000-0000-0000-000000000000",
130+
SensitivityLabel2: null,
131+
SetOwnerWithoutUpdatingSecondaryAdmin: false,
132+
SharingAllowedDomainList: "",
133+
SharingBlockedDomainList: "",
134+
SharingCapability: 0,
135+
SharingDomainRestrictionMode: 0,
136+
SharingLockDownCanBeCleared: true,
137+
SharingLockDownEnabled: false,
138+
ShowPeoplePickerSuggestionsForGuestUsers: false,
139+
SiteDefinedSharingCapability: 0,
140+
SiteId: "8f6fdeda-f6ff-4d39-8a8c-fe86565afefd",
141+
SocialBarOnSitePagesDisabled: false,
142+
Status: "Active",
143+
StorageMaximumLevel: "26214400",
144+
StorageQuotaType: null,
145+
StorageUsage: "3",
146+
StorageWarningLevel: "25574400",
147+
TeamsChannelType: 0,
148+
Template: "SITEPAGEPUBLISHING#0",
149+
TimeZoneId: 2,
150+
Title: "Marketing and Communications",
151+
TitleTranslations: null,
152+
Url: "https://contoso.sharepoint.com/sites/marketing",
153+
UserCodeMaximumLevel: 300,
154+
UserCodeWarningLevel: 200,
155+
VersionCount: "7",
156+
VersionPolicyFileTypeOverride: [],
157+
VersionSize: "0",
158+
WebsCount: 1
159+
};
160+
161+
before(() => {
162+
sinon.stub(auth, 'restoreAuth').resolves();
163+
sinon.stub(telemetry, 'trackEvent').resolves();
164+
sinon.stub(pid, 'getProcessName').returns('');
165+
sinon.stub(session, 'getId').returns('');
166+
auth.connection.active = true;
167+
commandInfo = cli.getCommandInfo(command);
168+
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName: string, defaultValue: any) => {
169+
if (settingName === settingsNames.prompt) {
170+
return false;
171+
}
172+
173+
return defaultValue;
174+
});
175+
});
176+
177+
beforeEach(() => {
178+
log = [];
179+
logger = {
180+
log: async (msg: string) => {
181+
log.push(msg);
182+
},
183+
logRaw: async (msg: string) => {
184+
log.push(msg);
185+
},
186+
logToStderr: async (msg: string) => {
187+
log.push(msg);
188+
}
189+
};
190+
loggerLogSpy = sinon.spy(logger, 'log');
191+
sinon.stub(spo, 'getSpoAdminUrl').resolves(adminUrl);
192+
});
193+
194+
afterEach(() => {
195+
sinonUtil.restore([
196+
request.get,
197+
request.post,
198+
cli.handleMultipleResultsFound,
199+
spo.getSpoAdminUrl,
200+
spo.getSiteAdminPropertiesByUrl
201+
]);
202+
});
203+
204+
after(() => {
205+
sinon.restore();
206+
auth.connection.active = false;
207+
});
208+
209+
it('has correct name', () => {
210+
assert.strictEqual(command.name, commands.TENANT_SITE_GET);
211+
});
212+
213+
it('has a description', () => {
214+
assert.notStrictEqual(command.description, null);
215+
});
216+
217+
it('retrieves site by id', async () => {
218+
sinon.stub(request, 'get').callsFake(async (opts) => {
219+
if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites('${siteId}')?$select=Url`) {
220+
return { Url: siteUrl };
221+
}
222+
throw 'Invalid request';
223+
});
224+
225+
sinon.stub(spo, 'getSiteAdminPropertiesByUrl').resolves(siteResponse);
226+
227+
await command.action(logger, { options: { id: siteId, verbose: true } });
228+
assert(loggerLogSpy.calledWith(siteResponse));
229+
});
230+
231+
it('retrieves site by url', async () => {
232+
sinon.stub(spo, 'getSiteAdminPropertiesByUrl').resolves(siteResponse);
233+
234+
await command.action(logger, { options: { url: siteUrl, verbose: true } });
235+
assert(loggerLogSpy.calledWith(siteResponse));
236+
});
237+
238+
it('retrieves site by title (single result)', async () => {
239+
sinon.stub(request, 'post').callsFake(async (opts) => {
240+
if (opts.url === `${adminUrl}/_api/web/lists/GetByTitle('DO_NOT_DELETE_SPLIST_TENANTADMIN_AGGREGATED_SITECOLLECTIONS')/RenderListDataAsStream`) {
241+
return { Row: [{ Title: 'Marketing', SiteUrl: siteUrl, SiteId: `/Guid(${siteId})/` }] };
242+
}
243+
throw 'Invalid request';
244+
});
245+
246+
sinon.stub(spo, 'getSiteAdminPropertiesByUrl').resolves(siteResponse);
247+
248+
await command.action(logger, { options: { title: 'Marketing', verbose: true } });
249+
assert(loggerLogSpy.calledWith(siteResponse));
250+
});
251+
252+
it('retrieves site by title (multiple results prompts)', async () => {
253+
sinon.stub(request, 'post').callsFake(async (opts) => {
254+
if (opts.url === `${adminUrl}/_api/web/lists/GetByTitle('DO_NOT_DELETE_SPLIST_TENANTADMIN_AGGREGATED_SITECOLLECTIONS')/RenderListDataAsStream`) {
255+
return {
256+
Row: [
257+
{ Title: 'Marketing', SiteUrl: siteUrl, SiteId: `/Guid(${siteId})/` },
258+
{ Title: 'Marketing', SiteUrl: 'https://contoso.sharepoint.com/sites/Marketing2', SiteId: '/Guid(53dec431-9d4f-415b-b12b-010259d5b4e1)/' }
259+
]
260+
};
261+
}
262+
throw 'Invalid request';
263+
});
264+
265+
sinon.stub(cli, 'handleMultipleResultsFound').resolves({ url: siteUrl } as any);
266+
267+
sinon.stub(spo, 'getSiteAdminPropertiesByUrl').resolves(siteResponse);
268+
269+
await command.action(logger, { options: { title: 'Marketing', verbose: true } });
270+
assert(loggerLogSpy.calledWith(siteResponse));
271+
});
272+
273+
it('handles error when specified site by title not found', async () => {
274+
sinon.stub(request, 'post').callsFake(async (opts) => {
275+
if (opts.url === `${adminUrl}/_api/web/lists/GetByTitle('DO_NOT_DELETE_SPLIST_TENANTADMIN_AGGREGATED_SITECOLLECTIONS')/RenderListDataAsStream`) {
276+
return { Row: [] };
277+
}
278+
throw 'Invalid request';
279+
});
280+
281+
await assert.rejects(command.action(logger, { options: { title: 'Marketing', verbose: true } } as any), new CommandError("The specified site 'Marketing' does not exist."));
282+
});
283+
284+
it('fails validation when specifying none of id, title, url', async () => {
285+
const commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
286+
const refined = commandInfo.command.getRefinedSchema!(commandOptionsSchema as any)!;
287+
const actual = refined.safeParse({});
288+
assert.strictEqual(actual.success, false);
289+
});
290+
291+
it('fails validation when specifying multiple of id, title, url', async () => {
292+
const commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
293+
const refined = commandInfo.command.getRefinedSchema!(commandOptionsSchema as any)!;
294+
const actual = refined.safeParse({ id: siteId, url: siteUrl });
295+
assert.strictEqual(actual.success, false);
296+
});
297+
298+
it('handles OData error when site not found', async () => {
299+
sinon.stub(request, 'get').callsFake(async (opts) => {
300+
if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites('${siteId}')?$select=Url`) {
301+
const error = {
302+
error: {
303+
'odata.error': {
304+
code: '-2147024891, System.UnauthorizedAccessException',
305+
message: {
306+
lang: 'en-US',
307+
value: 'Attempted to perform an unauthorized operation.'
308+
}
309+
}
310+
}
311+
};
312+
throw error;
313+
}
314+
throw 'Invalid request';
315+
});
316+
317+
await assert.rejects(command.action(logger, { options: { id: siteId, verbose: true } } as any), new CommandError('Attempted to perform an unauthorized operation.'));
318+
});
319+
});
320+
321+

0 commit comments

Comments
 (0)