diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d828a4f..2b052fa 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -12,5 +12,6 @@ module.exports = { semi: "error", "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-array-constructor": "off", }, }; diff --git a/package-lock.json b/package-lock.json index cab3144..e4e7190 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "msw-sp", - "version": "1.10.0", + "version": "1.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "msw-sp", - "version": "1.10.0", + "version": "1.11.0", "license": "ISC", "dependencies": { "eh-odata-parser": "^1.4.3", diff --git a/package.json b/package.json index 0129a5d..ebd142f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "msw-sp", - "version": "1.10.0", + "version": "1.11.0", "description": "MSW handlers for mocking SharePoint REST api.", "main": "lib/index.js", "engines": { diff --git a/src/handlers.ts b/src/handlers.ts index 392d62a..86e3d48 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -274,6 +274,10 @@ export const handlers = (options: Tenant | { tenant: Tenant, delay?: DelayMode | const title = info.params.title.toString(); return response(await tenantMock.sites.getSite(site).rootWeb.lists.getByTitle(title).rootFolder.get(), info); }), + ...get("/_api/web/defaultDocumentLibrary/items", async (info) => { + const site = info.params.site?.toString() || "/"; + return response(await tenantMock.sites.getSite(site).rootWeb.defaultDocumentLibrary.items.get(), info); + }), ...get("/_api/web/lists\\(':listId'\\)/items", async (info) => { const site = info.params.site?.toString() || "/"; const listId = info.params.listId.toString(); @@ -323,6 +327,10 @@ export const handlers = (options: Tenant | { tenant: Tenant, delay?: DelayMode | const site = info.params.site?.toString() || "/"; return response(await tenantMock.sites.getSite(site).rootWeb.defaultDocumentLibrary.rootFolder.get(), info); }), + ...get("/_api/web/defaultDocumentLibrary/rootFolder/files", async (info) => { + const site = info.params.site?.toString() || "/"; + return response(await tenantMock.sites.getSite(site).rootWeb.defaultDocumentLibrary.rootFolder.files.get(), info); + }), ...get("/_api/web/defaultDocumentLibrary/getitems", async (info) => { const site = info.params.site?.toString() || "/"; return response(await tenantMock.sites.getSite(site).rootWeb.defaultDocumentLibrary.items.get(), info); @@ -353,6 +361,11 @@ export const handlers = (options: Tenant | { tenant: Tenant, delay?: DelayMode | const id = info.params.id.toString(); return response(await tenantMock.sites.getSite(site).rootWeb.siteUsers.getById(id).get(), info); }), + ...get("/_api/web/getFileById\\(':id'\\)", async (info) => { + const site = info.params.site?.toString() || "/"; + const id = info.params.id.toString(); + return response(await tenantMock.sites.getSite(site).rootWeb.getFileById(id).get(), info); + }), ...get("/_api/*", async (info) => { return response(new Response(undefined, { status: 501, statusText: "Not Implemented (GET)" }), info); }), @@ -383,6 +396,28 @@ export const handlers = (options: Tenant | { tenant: Tenant, delay?: DelayMode | const payload = await info.request.json(); return response(await tenantMock.sites.getSite(site).rootWeb.lists.getByTitle(title).items.post(payload), info); }), + ...post("/_api/web/lists/getByTitle\\(':title'\\)", async (info) => { + const site = info.params.site?.toString() || "/"; + const title = info.params.title.toString(); + + if (info.request.headers.get("x-http-method") === "DELETE") { + return response(await tenantMock.sites.getSite(site).rootWeb.lists.getByTitle(title).delete(), info); + } + + const payload = await info.request.json(); + return response(await tenantMock.sites.getSite(site).rootWeb.lists.getByTitle(title).post(payload), info); + }), + ...post("/_api/web/lists\\(':listId'\\)", async (info) => { + const site = info.params.site?.toString() || "/"; + const listId = info.params.listId.toString(); + + if (info.request.headers.get("x-http-method") === "DELETE") { + return response(await tenantMock.sites.getSite(site).rootWeb.lists.getById(listId).delete(), info); + } + + const payload = await info.request.json(); + return response(await tenantMock.sites.getSite(site).rootWeb.lists.getById(listId).post(payload), info); + }), ...post("/_api/web/lists/getByTitle\\(':title'\\)/items\\(:id\\)", async (info) => { const site = info.params.site?.toString() || "/"; const title = info.params.title.toString(); diff --git a/src/mocks/FileMock.ts b/src/mocks/FileMock.ts new file mode 100644 index 0000000..5a23f4e --- /dev/null +++ b/src/mocks/FileMock.ts @@ -0,0 +1,25 @@ +import { Utils } from "../Utils.js"; +import type { File } from "../types/File.js"; + +/** + * @internal + */ +export class FileMock { + constructor(private files?: Array, private file?: File) { + } + + get = async () => { + if (!this.file) { + return new Response(undefined, { status: 404 }); + } + + const f = Utils.upperCaseKeys(this.file); + delete f.Content; + f.Exists = true; + + return new Response( + JSON.stringify(f), + { status: 200 }, + ); + }; +} \ No newline at end of file diff --git a/src/mocks/FilesMock.ts b/src/mocks/FilesMock.ts new file mode 100644 index 0000000..1bcb700 --- /dev/null +++ b/src/mocks/FilesMock.ts @@ -0,0 +1,28 @@ +import type { File } from '../types/File.js'; +import { FileMock } from './FileMock.js'; + +/** + * @internal + */ +export class FilesMock { + constructor(private files?: Array) { + } + + get = async () => { + if (!this.files) { + return new Response(undefined, { status: 404 }); + } + + const mocks = this.files.map(file => new FileMock(this.files, file)); + const infos = new Array(); + for (let i = 0; i !== mocks.length; i++) { + const list = await mocks[i].get(); + infos.push(await list.json()); + } + + return new Response( + JSON.stringify(infos), + { status: 200 }, + ); + }; +} \ No newline at end of file diff --git a/src/mocks/FolderMock.ts b/src/mocks/FolderMock.ts index 8b5f162..448176b 100644 --- a/src/mocks/FolderMock.ts +++ b/src/mocks/FolderMock.ts @@ -1,5 +1,6 @@ import { Utils } from "../Utils.js"; import type { Folder } from "../types/Folder.js"; +import { FilesMock } from "./FilesMock.js"; /** * @internal @@ -8,6 +9,10 @@ export class FolderMock { constructor(private folder: Folder) { } + public get files() { + return new FilesMock(this.folder.files); + } + get = async () => { if (!this.folder) { return new Response(undefined, { status: 404 }); diff --git a/src/mocks/ListMock.ts b/src/mocks/ListMock.ts index 7c0a8dc..d7da8f8 100644 --- a/src/mocks/ListMock.ts +++ b/src/mocks/ListMock.ts @@ -1,3 +1,4 @@ +import type { DefaultBodyType } from "msw"; import { Utils } from "../Utils.js"; import type { List } from "../types/List.js"; import { BasePermissionsMock } from "./BasePermissionsMock.js"; @@ -11,7 +12,7 @@ import { ViewsMock } from "./ViewsMock.js"; * @internal */ export class ListMock { - constructor(private list?: List) { + constructor(private lists?: Array, private list?: List) { } public get fields() { @@ -35,6 +36,9 @@ export class ListMock { } public get rootFolder() { + if (this.list && "rootFolder" in this.list && this.list.rootFolder) { + return new FolderMock(this.list.rootFolder); + } return new FolderMock({ name: this.list?.title }); } @@ -107,4 +111,34 @@ export class ListMock { { status: 200 }, ); }; + + post = async (payload: DefaultBodyType) => { + if (!this.list) { + return new Response(undefined, { status: 404 }); + } + + Object.assign(this.list, payload); + + return new Response( + JSON.stringify({}), + { status: 200 }, + ); + }; + + delete = async () => { + if (!this.list) { + return new Response(undefined, { status: 404 }); + } + + const index = this.lists?.findIndex(i => i.id === this.list!.id); + this.list = undefined; + if (index !== undefined) { + this.lists?.splice(index, 1); + } + + return new Response( + JSON.stringify({}), + { status: 200 }, + ); + }; } \ No newline at end of file diff --git a/src/mocks/ListsMock.ts b/src/mocks/ListsMock.ts index 2a4b2c3..546c2b8 100644 --- a/src/mocks/ListsMock.ts +++ b/src/mocks/ListsMock.ts @@ -12,12 +12,12 @@ export class ListsMock { getByTitle = (title: string) => { const list = this.lists?.find(list => list.title === title); - return new ListMock(list); + return new ListMock(this.lists, list); }; getById = (id: string) => { const list = this.lists?.find(list => list.id === id); - return new ListMock(list); + return new ListMock(this.lists, list); }; get = async () => { @@ -25,7 +25,7 @@ export class ListsMock { return new Response(undefined, { status: 404 }); } - const mocks = this.lists.map(list => new ListMock(list)); + const mocks = this.lists.map(list => new ListMock(this.lists, list)); const infos = new Array(); for (let i = 0; i !== mocks.length; i++) { const list = await mocks[i].get(); @@ -48,6 +48,6 @@ export class ListsMock { list.items = []; this.lists.push(list); - return await new ListMock(list).get(); + return await new ListMock(this.lists, list).get(); }; } \ No newline at end of file diff --git a/src/mocks/WebMock.ts b/src/mocks/WebMock.ts index 2568035..2d63d8c 100644 --- a/src/mocks/WebMock.ts +++ b/src/mocks/WebMock.ts @@ -1,7 +1,9 @@ import { Utils } from "../Utils.js"; +import type { Folder } from "../types/Folder.js"; import type { Site } from "../types/Site.js"; import type { Web } from "../types/Web.js"; import { BasePermissionsMock } from "./BasePermissionsMock.js"; +import { FileMock } from "./FileMock.js"; import { ListMock } from "./ListMock.js"; import { ListsMock } from "./ListsMock.js"; import { UsersMock } from "./UsersMock.js"; @@ -28,7 +30,7 @@ export class WebMock { public get defaultDocumentLibrary() { const list = this.web.lists?.find(l => l.baseTemplate === 101 && l.isDefaultDocumentLibrary); - return new ListMock(list); + return new ListMock(this.web?.lists, list); } public ensureUser(payload: { logonName: string }) { @@ -51,7 +53,45 @@ export class WebMock { return Utils.urls.equals(url, listRelativeUrl); }); - return new ListMock(list); + return new ListMock(this.web?.lists, list); + }; + + getFileById = (id: string) => { + if (!this.web.lists) { + return new FileMock(undefined, undefined); + } + for (let i = 0; i !== this.web.lists?.length; i++) { + const list = this.web.lists[i]; + if (list.baseTemplate !== 101 || !list.rootFolder) { + continue; + } + + const getFile = (folder: Folder) => { + if (!folder) { + return undefined; + } + let file = folder.files?.find(f => f.uniqueId === id); + if (file) { + return file; + } + if (folder.folders) { + for (let i = 0; i !== folder.folders?.length; i++) { + file = getFile(folder.folders[i]); + if (file) { + return file; + } + } + } + return undefined; + }; + + const file = getFile(list.rootFolder); + if (file) { + return new FileMock(list.rootFolder.files, file); + } + } + + return new FileMock(undefined, undefined); }; get = async () => { diff --git a/src/tests/files.test.ts b/src/tests/files.test.ts new file mode 100644 index 0000000..8c6834d --- /dev/null +++ b/src/tests/files.test.ts @@ -0,0 +1,82 @@ +import { SPFx, spfi } from "@pnp/sp"; +import "@pnp/sp/fields/index.js"; +import "@pnp/sp/files/index.js"; +import "@pnp/sp/folders/index.js"; +import "@pnp/sp/items/index.js"; +import "@pnp/sp/lists/index.js"; +import "@pnp/sp/site-users/index.js"; +import "@pnp/sp/sites/index.js"; +import "@pnp/sp/webs/index.js"; +import { setupServer } from 'msw/node'; +import assert from "node:assert"; +import { describe, test } from "node:test"; +import { handlers } from '../handlers.js'; + +void describe("files", async () => { + const url = "https://tenant.sharepoint.com"; + const server = setupServer(...handlers({ + title: "tenant", + url, + sites: { + "files": { + rootWeb: { + title: "Files Site", + serverRelativeUrl: "/sites/files", + lists: [ + { + title: "Documents", + id: "c4a8690d-678f-47c9-a1b1-7fb0837254a1", + baseTemplate: 101, + url: "Lists/Documents", + isDefaultDocumentLibrary: true, + items: [], + rootFolder: { + files: [ + { + uniqueId: "7d131b18-9ff1-43e5-9c76-42ee9958088d", + name: "Package.txt", + content: "msw-sp", + }, + ] + } + }, + ], + }, + }, + + }, + })); + server.listen(); + + const getContext = (serverRelativeUrl: string) => { + return { + pageContext: { + web: { + absoluteUrl: `${url}${serverRelativeUrl}`, + }, + legacyPageContext: { + formDigestTimeoutSeconds: 60, + formDigestValue: "digest", + }, + }, + }; + }; + + await test("files", async () => { + const sp = spfi().using(SPFx(getContext("/sites/files"))); + + const fileInfos = await sp.web.defaultDocumentLibrary.rootFolder.files(); + assert.ok(fileInfos); + assert.equal(fileInfos.length, 1); + let fileInfo = fileInfos[0]; + assert.equal(fileInfo.UniqueId, "7d131b18-9ff1-43e5-9c76-42ee9958088d"); + assert.equal(fileInfo.Name, "Package.txt"); + + const file = sp.web.getFileById(fileInfo.UniqueId); + fileInfo = await file(); + assert.equal(fileInfo.Name, "Package.txt"); + + const exists = await file.exists(); + assert.ok(exists); + }); +}); \ No newline at end of file diff --git a/src/tests/items.test.ts b/src/tests/items.test.ts index ddbb75a..20f68ad 100644 --- a/src/tests/items.test.ts +++ b/src/tests/items.test.ts @@ -91,4 +91,25 @@ void describe("items", async () => { items = await list.items(); assert.equal(items.length, 0); }); + + await test("items, async Iterator", async () => { + const sp = spfi().using(SPFx(getContext("/sites/items"))); + + const list = sp.web.lists.getByTitle("List"); + + const items = await list.items(); + assert.equal(items.length, 0); + + // Add + for (let i = 0; i !== 55; i++) { + await list.items.add({ Title: `New ${i}` }); + } + + let array = new Array(); + for await (const page of list.items) { + array = array.concat(page); + } + + assert.equal(array.length, 55); + }); }); \ No newline at end of file diff --git a/src/tests/lists.test.ts b/src/tests/lists.test.ts index 69b2c2c..9ed30bb 100644 --- a/src/tests/lists.test.ts +++ b/src/tests/lists.test.ts @@ -169,23 +169,40 @@ void describe("lists", async () => { assert.equal(eventListInfo.LastItemModifiedDate, "2024-01-04T11:56:54Z"); }); - await test("add", async () => { + await test("crud", async () => { const sp = spfi().using(SPFx(getContext("/sites/events"))); - const title = new Date().getTime().toString(); + // Create + let title = new Date().getTime().toString(); const listInfo = await sp.web.lists.add(title, "Description", 100, true, { NoCrawl: true, }); assert.equal(listInfo.Title, title); - const lists = await sp.web.lists(); - const list = lists.find(l => l.Title === title); + let lists = await sp.web.lists(); + let list = lists.find(l => l.Title === title); assert.ok(list); + assert.ok(list.Id); assert.equal(list.Description, "Description"); assert.equal(list.BaseTemplate, 100); assert.equal(list.ContentTypesEnabled, true); assert.equal(list.NoCrawl, true); + + // Update + title = `${listInfo.Title} update`; + await sp.web.lists.getById(list.Id).update({ + Title: title, + }); + lists = await sp.web.lists(); + list = lists.find(l => l.Title === title); + assert.ok(list); + + // Delete + await sp.web.lists.getById(list.Id).delete(); + lists = await sp.web.lists(); + list = lists.find(l => l.Title === title); + assert.ok(list === undefined); }); await test("fields", async () => { diff --git a/src/types/File.ts b/src/types/File.ts new file mode 100644 index 0000000..feecb73 --- /dev/null +++ b/src/types/File.ts @@ -0,0 +1,8 @@ + +export type File = { + uniqueId?: string; + name?: string; + content?: string; + + listItemAllFields?: Record; +} \ No newline at end of file diff --git a/src/types/Folder.ts b/src/types/Folder.ts index afeac0d..d62699e 100644 --- a/src/types/Folder.ts +++ b/src/types/Folder.ts @@ -1,4 +1,9 @@ +import type { File } from "./File.js"; export type Folder = { name?: string; + + files?: Array; + folders?: Array; + uniqueContentTypeOrder?: Array<{ stringValue: string }>; } \ No newline at end of file diff --git a/src/types/List.ts b/src/types/List.ts index 9385e47..283ff50 100644 --- a/src/types/List.ts +++ b/src/types/List.ts @@ -1,5 +1,6 @@ import type { ContentType } from "./ContentType.js"; import type { Field } from "./Field.js"; +import type { Folder } from "./Folder.js"; import type { View } from "./View.js"; export type List = { @@ -52,6 +53,8 @@ export type DocumentLibraryList = { baseTemplate: 101; items: Array>; isDefaultDocumentLibrary?: boolean; + + rootFolder?: Folder; } export type SitePagesList = {