microsoft-onedrive-mock 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.ENV_EXAMPLE ADDED
@@ -0,0 +1,16 @@
1
+ # To run tests against the real Microsoft Graph API
2
+ # Copy this file to .ENV and fill in the values
3
+
4
+ # 'mock' (default) or 'real'
5
+ TEST_TARGET=real
6
+
7
+ # OAuth2 Access Token for your Microsoft Account
8
+ # Get one via 'npm run example:login'
9
+ ONEDRIVE_TOKEN=...
10
+
11
+ # Optional: Simulate latency in ms (only works with TEST_TARGET=mock)
12
+ # LATENCY=50
13
+
14
+ # Client ID for the Microsoft Login Example
15
+ ONEDRIVE_CLIENT_ID=e5...
16
+ ONEDRIVE_TENANT_ID=common
@@ -0,0 +1,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+ pull_request:
7
+ branches: [ main, master ]
8
+
9
+ jobs:
10
+ build-and-test:
11
+ runs-on: ubuntu-24.04
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Use Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '24'
20
+ cache: 'npm'
21
+
22
+ - name: Install dependencies
23
+ run: npm install
24
+
25
+ - name: Lint
26
+ run: npm run lint
27
+
28
+ - name: Build
29
+ run: npm run build
30
+
31
+ - name: Test (Node)
32
+ run: npm test
33
+
34
+ - name: Install Playwright Browsers
35
+ run: npx playwright@1.58.0 install chromium
36
+
37
+ - name: Test (Browser)
38
+ run: npm run test:browser
39
+
40
+ - name: Test Slow
41
+ run: npm run test:slow
@@ -0,0 +1,50 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version:
7
+ description: 'New Version (e.g. 1.0.0, or leave empty for patch)'
8
+ required: false
9
+ type: string
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-24.04
14
+ # @link https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
15
+ permissions:
16
+ contents: write
17
+ # @link https://docs.npmjs.com/generating-provenance-statements#about-npm-provenance
18
+ id-token: write
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-node@v4
22
+ with:
23
+ node-version: '24'
24
+ registry-url: 'https://registry.npmjs.org'
25
+ cache: 'npm'
26
+
27
+ - run: npm install
28
+ - name: Bump Version
29
+ id: bump
30
+ run: |
31
+ if [ -z "${{ inputs.version }}" ]; then
32
+ VERSION=$(npm version patch --no-git-tag-version)
33
+ else
34
+ VERSION=$(npm version ${{ inputs.version }} --no-git-tag-version)
35
+ fi
36
+ echo "version=${VERSION}" >> $GITHUB_OUTPUT
37
+ - run: npm run build
38
+ - run: npm run lint
39
+ - run: npm test
40
+ - run: npm publish
41
+ env:
42
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
43
+
44
+ - name: Push changes
45
+ run: |
46
+ git config --global user.name 'github-actions[bot]'
47
+ git config --global user.email 'github-actions[bot]@users.noreply.github.com'
48
+ git add package.json package-lock.json
49
+ git commit -m "release: ${{ steps.bump.outputs.version }}"
50
+ git push
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # microsoft-onedrive-mock
@@ -0,0 +1,2 @@
1
+ import { Request, Response } from 'express';
2
+ export declare const handleBatchRequest: (req: Request, res: Response) => Promise<void>;
package/dist/batch.js ADDED
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.handleBatchRequest = void 0;
13
+ const index_1 = require("./index");
14
+ const handleBatchRequest = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
15
+ try {
16
+ const body = req.body;
17
+ if (!body || !Array.isArray(body.requests)) {
18
+ res.status(400).json({ error: { message: "Invalid batch payload" } });
19
+ return;
20
+ }
21
+ // We use an internal app instance without auth/latency middleware to process routes cleanly
22
+ const internalApp = (0, index_1.createApp)({ serverLagBefore: 0, serverLagAfter: 0 });
23
+ const batchResponses = yield Promise.all(body.requests.map((part) => __awaiter(void 0, void 0, void 0, function* () {
24
+ const { id, method, url, body: partBody, headers: partHeaders } = part;
25
+ // Reconstruct path. Graph batches often send relative urls like /me/drive/root
26
+ // We append /v1.0/ to map to our internal routes if missing
27
+ const requestPath = url.startsWith('/v1.0') ? url : `/v1.0${url.startsWith('/') ? '' : '/'}${url}`;
28
+ return new Promise((resolve) => {
29
+ const simulatedReq = {
30
+ method: method || 'GET',
31
+ url: requestPath,
32
+ headers: partHeaders || {},
33
+ body: partBody,
34
+ query: {}
35
+ };
36
+ // Inject auth header from primary request to pass auth middleware
37
+ if (req.headers.authorization && !simulatedReq.headers.authorization) {
38
+ simulatedReq.headers.authorization = req.headers.authorization;
39
+ }
40
+ const simulatedRes = {
41
+ statusCode: 200,
42
+ headers: {},
43
+ charset: 'utf-8',
44
+ status: function (code) {
45
+ this.statusCode = code;
46
+ return this;
47
+ },
48
+ set: function (headerKey, headerValue) {
49
+ this.headers[headerKey] = headerValue;
50
+ return this;
51
+ },
52
+ setHeader: function (headerKey, headerValue) {
53
+ this.headers[headerKey] = headerValue;
54
+ return this;
55
+ },
56
+ json: function (data) {
57
+ this.data = data;
58
+ this.end();
59
+ },
60
+ send: function (data) {
61
+ // Normally this would be string/buffer, but let's just hold it
62
+ this.data = data;
63
+ this.end();
64
+ },
65
+ end: function () {
66
+ resolve({
67
+ id,
68
+ status: this.statusCode,
69
+ headers: this.headers,
70
+ body: this.data
71
+ });
72
+ }
73
+ };
74
+ // Bypass async handlers and directly feed to internal instance
75
+ internalApp(simulatedReq, simulatedRes, () => {
76
+ // Fallback next
77
+ resolve({
78
+ id,
79
+ status: 404,
80
+ body: { error: { message: "Not found within batch router" } }
81
+ });
82
+ });
83
+ });
84
+ })));
85
+ res.json({
86
+ responses: batchResponses
87
+ });
88
+ }
89
+ catch (error) {
90
+ res.status(500).json({ error: { message: "Internal server error during batch", details: error.message } });
91
+ }
92
+ });
93
+ exports.handleBatchRequest = handleBatchRequest;
@@ -0,0 +1,6 @@
1
+ import { driveStore } from './store';
2
+ import { AppConfig } from './types';
3
+ export * from './types';
4
+ declare const createApp: (config?: AppConfig) => import("express-serve-static-core").Express;
5
+ declare const startServer: (port: number, host?: string, config?: AppConfig) => import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>;
6
+ export { createApp, startServer, driveStore };
package/dist/index.js ADDED
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
17
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
18
+ return new (P || (P = Promise))(function (resolve, reject) {
19
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
20
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
21
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
22
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
23
+ });
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.driveStore = exports.startServer = exports.createApp = void 0;
30
+ const express_1 = __importDefault(require("express"));
31
+ const cors_1 = __importDefault(require("cors"));
32
+ const store_1 = require("./store");
33
+ Object.defineProperty(exports, "driveStore", { enumerable: true, get: function () { return store_1.driveStore; } });
34
+ const v1_1 = require("./routes/v1");
35
+ const batch_1 = require("./batch");
36
+ __exportStar(require("./types"), exports);
37
+ const createApp = (config = {}) => {
38
+ if (!config.apiEndpoint) {
39
+ config.apiEndpoint = "";
40
+ }
41
+ const app = (0, express_1.default)();
42
+ app.use((0, cors_1.default)({
43
+ // For downloads Microsoft uses specific headers sometimes, expose ETag
44
+ exposedHeaders: ['ETag', 'Date', 'Content-Length', 'Location']
45
+ }));
46
+ app.set('etag', false);
47
+ // Latency simulator
48
+ app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
49
+ const delay = Math.floor(Math.random() * 21);
50
+ if (delay > 0) {
51
+ yield new Promise(resolve => setTimeout(resolve, delay));
52
+ }
53
+ next();
54
+ }));
55
+ app.use(express_1.default.json({
56
+ verify: (req, res, buf) => {
57
+ req.rawBody = buf;
58
+ }
59
+ }));
60
+ app.use(express_1.default.text({
61
+ type: ['multipart/mixed', 'multipart/related', 'text/*', 'application/xml', 'application/octet-stream'],
62
+ verify: (req, res, buf) => {
63
+ req.rawBody = buf;
64
+ }
65
+ }));
66
+ // Explicit raw body for binary uploads
67
+ app.use(express_1.default.raw({
68
+ type: '*/*',
69
+ limit: '50mb',
70
+ verify: (req, res, buf) => {
71
+ req.rawBody = buf;
72
+ }
73
+ }));
74
+ // Batch Route
75
+ app.post('/v1.0/$batch', batch_1.handleBatchRequest);
76
+ // Debug
77
+ app.post('/debug/clear', (req, res) => {
78
+ store_1.driveStore.clear();
79
+ res.status(200).send('Cleared');
80
+ });
81
+ // Health Check
82
+ app.get('/', (req, res) => {
83
+ res.status(200).send('OK');
84
+ });
85
+ // Auth Middleware
86
+ const validTokens = ['valid-token', 'another-valid-token'];
87
+ app.use((req, res, next) => {
88
+ const authHeaderVal = req.headers.authorization;
89
+ const authHeader = Array.isArray(authHeaderVal) ? authHeaderVal[0] : authHeaderVal;
90
+ if (!authHeader) {
91
+ res.status(401).json({ error: { code: "unauthenticated", message: "Unauthorized: No token provided" } });
92
+ return;
93
+ }
94
+ const token = authHeader.split(' ')[1];
95
+ if (!validTokens.includes(token)) {
96
+ res.status(401).json({ error: { code: "unauthenticated", message: "Unauthorized: Invalid token" } });
97
+ return;
98
+ }
99
+ next();
100
+ });
101
+ app.use((0, v1_1.createV1Router)());
102
+ return app;
103
+ };
104
+ exports.createApp = createApp;
105
+ const startServer = (port, host = 'localhost', config = {}) => {
106
+ const app = createApp(config);
107
+ return app.listen(port, host, () => {
108
+ console.log(`Server is running on http://${host}:${port}`);
109
+ });
110
+ };
111
+ exports.startServer = startServer;
112
+ if (require.main === module) {
113
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3006;
114
+ startServer(port);
115
+ }
@@ -0,0 +1 @@
1
+ export declare const createV1Router: () => import("express-serve-static-core").Router;
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createV1Router = void 0;
7
+ const express_1 = __importDefault(require("express"));
8
+ const store_1 = require("../store");
9
+ const createV1Router = () => {
10
+ const app = express_1.default.Router();
11
+ // Helper to apply ?$select=id,name
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ const applySelect = (item, selectQuery) => {
14
+ if (!selectQuery || typeof selectQuery !== 'string')
15
+ return item;
16
+ const fields = selectQuery.split(',').map(f => f.trim());
17
+ if (fields.length === 0)
18
+ return item;
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ const result = {};
21
+ for (const field of fields) {
22
+ if (item[field] !== undefined) {
23
+ result[field] = item[field];
24
+ }
25
+ }
26
+ return result;
27
+ };
28
+ // GET /me/drive/root
29
+ app.get('/v1.0/me/drive/root', (req, res) => {
30
+ const root = store_1.driveStore.getItem('root');
31
+ if (!root)
32
+ return res.status(404).json({ error: { message: "Root not found" } });
33
+ res.json(applySelect(root, req.query.$select));
34
+ });
35
+ // GET /me/drive/items/{id}
36
+ app.get('/v1.0/me/drive/items/:itemId', (req, res) => {
37
+ let itemId = req.params.itemId;
38
+ if (itemId === 'root')
39
+ itemId = 'root'; // Already handled if just mapping
40
+ const item = store_1.driveStore.getItem(itemId);
41
+ if (!item) {
42
+ res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
43
+ return;
44
+ }
45
+ res.json(applySelect(item, req.query.$select));
46
+ });
47
+ // GET /me/drive/items/{id}/children
48
+ app.get('/v1.0/me/drive/items/:itemId/children', (req, res) => {
49
+ const itemId = req.params.itemId;
50
+ const children = store_1.driveStore.listItems(itemId);
51
+ const mappedChildren = children.map(c => applySelect(c, req.query.$select));
52
+ res.json({ value: mappedChildren });
53
+ });
54
+ // GET /me/drive/root/search(q='query')
55
+ app.get('/v1.0/me/drive/root/search\\(q=\':query\'\\)', (req, res) => {
56
+ let query = req.params.query || "";
57
+ // Decode URI component incase it's URL encoded like %20 or %27
58
+ query = decodeURIComponent(query);
59
+ // Strip out the trailing quote matching if it caught the literal '
60
+ if (query.endsWith("'"))
61
+ query = query.slice(0, -1);
62
+ query = query.toLowerCase();
63
+ // recursively find items
64
+ const allItems = store_1.driveStore.getAllItems();
65
+ const results = allItems.filter(item => item.name.toLowerCase().includes(query) && item.id !== 'root');
66
+ const mappedResults = results.map(c => applySelect(c, req.query.$select));
67
+ res.json({ value: mappedResults });
68
+ });
69
+ // POST /me/drive/items/{parent-id}/children (Create Metadata / Folder)
70
+ app.post('/v1.0/me/drive/items/:itemId/children', (req, res) => {
71
+ const parentId = req.params.itemId;
72
+ const body = req.body || {};
73
+ let parentItem = store_1.driveStore.getItem(parentId);
74
+ if (!parentItem && parentId === 'root') {
75
+ parentItem = store_1.driveStore.createItem({ id: 'root', name: 'root' }, true);
76
+ }
77
+ else if (!parentItem) {
78
+ res.status(404).json({ error: { code: "itemNotFound", message: "Parent not found" } });
79
+ return;
80
+ }
81
+ const isFolder = !!body.folder;
82
+ const newItem = store_1.driveStore.createItem(Object.assign({ name: body.name || "Untitled", parentReference: { id: parentId } }, body), isFolder);
83
+ res.status(201).json(applySelect(newItem, req.query.$select));
84
+ });
85
+ // DELETE /me/drive/items/{id}
86
+ app.delete('/v1.0/me/drive/items/:itemId', (req, res) => {
87
+ const fileId = req.params.itemId;
88
+ if (fileId === 'root') {
89
+ res.status(400).json({ error: { message: "Cannot delete root" } });
90
+ return;
91
+ }
92
+ const item = store_1.driveStore.getItem(fileId);
93
+ if (!item) {
94
+ res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
95
+ return;
96
+ }
97
+ const ifMatch = req.header('If-Match');
98
+ if (ifMatch && ifMatch !== item.eTag) {
99
+ res.status(412).json({ error: { code: "PreconditionFailed", message: "ETag mismatch" } });
100
+ return;
101
+ }
102
+ store_1.driveStore.deleteItem(fileId);
103
+ res.status(204).send();
104
+ });
105
+ // GET /me/drive/items/{id}/content
106
+ app.get('/v1.0/me/drive/items/:itemId/content', (req, res) => {
107
+ var _a;
108
+ const fileId = req.params.itemId;
109
+ const file = store_1.driveStore.getItem(fileId);
110
+ if (!file || file.folder) {
111
+ res.status(404).json({ error: { code: "itemNotFound", message: "File not found" } });
112
+ return;
113
+ }
114
+ if ((_a = file.file) === null || _a === void 0 ? void 0 : _a.mimeType) {
115
+ res.setHeader('Content-Type', file.file.mimeType);
116
+ }
117
+ if (file.content === undefined) {
118
+ res.send("");
119
+ return;
120
+ }
121
+ if (Buffer.isBuffer(file.content)) {
122
+ res.send(file.content);
123
+ }
124
+ else if (typeof file.content === 'object') {
125
+ res.json(file.content);
126
+ }
127
+ else {
128
+ res.send(file.content);
129
+ }
130
+ });
131
+ // PUT /me/drive/items/{parent-id}:/{filename}:/content
132
+ app.put('/v1.0/me/drive/items/:parentId\\:/:filename\\:/content', (req, res) => {
133
+ const parentId = req.params.parentId;
134
+ const filename = req.params.filename;
135
+ // Find existing or create
136
+ const children = store_1.driveStore.listItems(parentId);
137
+ let item = children.find(c => c.name === filename);
138
+ const content = req.rawBody !== undefined ? req.rawBody : req.body;
139
+ const headerMime = req.headers['content-type'];
140
+ const mimeType = (Array.isArray(headerMime) ? headerMime[0] : headerMime) || 'application/octet-stream';
141
+ const isNew = !item;
142
+ if (item) {
143
+ const ifMatch = req.header('If-Match');
144
+ if (ifMatch && ifMatch !== item.eTag) {
145
+ res.status(412).json({ error: { code: "PreconditionFailed", message: "ETag mismatch" } });
146
+ return;
147
+ }
148
+ // Update
149
+ item = store_1.driveStore.updateItem(item.id, { content, file: { mimeType } });
150
+ }
151
+ else {
152
+ // Create
153
+ item = store_1.driveStore.createItem({
154
+ name: filename,
155
+ parentReference: { id: parentId },
156
+ content,
157
+ file: { mimeType }
158
+ });
159
+ }
160
+ res.status(isNew ? 201 : 200).json(applySelect(item, req.query.$select));
161
+ });
162
+ // PUT /me/drive/items/{id}/content
163
+ app.put('/v1.0/me/drive/items/:itemId/content', (req, res) => {
164
+ var _a;
165
+ const itemId = req.params.itemId;
166
+ let item = store_1.driveStore.getItem(itemId);
167
+ if (!item || item.folder) {
168
+ res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
169
+ return;
170
+ }
171
+ const ifMatch = req.header('If-Match');
172
+ if (ifMatch && ifMatch !== item.eTag) {
173
+ res.status(412).json({ error: { code: "PreconditionFailed", message: "ETag mismatch" } });
174
+ return;
175
+ }
176
+ const content = req.rawBody !== undefined ? req.rawBody : req.body;
177
+ const headerMime = req.headers['content-type'];
178
+ const mimeType = (Array.isArray(headerMime) ? headerMime[0] : headerMime) || ((_a = item.file) === null || _a === void 0 ? void 0 : _a.mimeType) || 'application/octet-stream';
179
+ item = store_1.driveStore.updateItem(item.id, { content, file: { mimeType } });
180
+ res.status(200).json(applySelect(item, req.query.$select));
181
+ });
182
+ // Delta Query
183
+ // GET /me/drive/root/delta
184
+ app.get('/v1.0/me/drive/root/delta', (req, res) => {
185
+ const tokenStr = req.query.token;
186
+ let token = undefined;
187
+ if (tokenStr)
188
+ token = tokenStr;
189
+ const result = store_1.driveStore.getDelta(token);
190
+ const host = req.headers.host || 'localhost';
191
+ const protocol = req.protocol || 'http';
192
+ const baseUrl = `${protocol}://${host}`;
193
+ res.json({
194
+ '@odata.context': `${baseUrl}/v1.0/$metadata#Collection(driveItem)`,
195
+ '@odata.deltaLink': `${baseUrl}/v1.0/me/drive/root/delta?token=${result.deltaLink}`,
196
+ value: result.items
197
+ });
198
+ });
199
+ return app;
200
+ };
201
+ exports.createV1Router = createV1Router;
@@ -0,0 +1,24 @@
1
+ import { DriveItem } from './types';
2
+ export declare class DriveStore {
3
+ private items;
4
+ private deltaHistory;
5
+ constructor();
6
+ private calculateStats;
7
+ createItem(item: Partial<DriveItem> & {
8
+ name: string;
9
+ }, isFolder?: boolean): DriveItem;
10
+ updateItem(id: string, updates: Partial<DriveItem>): DriveItem | null;
11
+ getItem(id: string): DriveItem | null;
12
+ deleteItem(id: string): boolean;
13
+ listItems(parentId?: string): DriveItem[];
14
+ getAllItems(): DriveItem[];
15
+ clear(): void;
16
+ private addDeltaHistory;
17
+ getDeltaToken(): string;
18
+ getDelta(token?: string): {
19
+ items: DriveItem[];
20
+ deltaLink: string;
21
+ };
22
+ private incrementParentChildCount;
23
+ }
24
+ export declare const driveStore: DriveStore;