microsoft-onedrive-mock 1.0.1 → 1.0.3

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/dist/index.js CHANGED
@@ -85,6 +85,8 @@ const createApp = (config = {}) => {
85
85
  // Auth Middleware
86
86
  const validTokens = ['valid-token', 'another-valid-token'];
87
87
  app.use((req, res, next) => {
88
+ if (req.path.startsWith('/v1.0/upload-sessions'))
89
+ return next();
88
90
  const authHeaderVal = req.headers.authorization;
89
91
  const authHeader = Array.isArray(authHeaderVal) ? authHeaderVal[0] : authHeaderVal;
90
92
  if (!authHeader) {
package/dist/routes/v1.js CHANGED
@@ -47,9 +47,84 @@ const createV1Router = () => {
47
47
  // GET /me/drive/items/{id}/children
48
48
  app.get('/v1.0/me/drive/items/:itemId/children', (req, res) => {
49
49
  const itemId = req.params.itemId;
50
- const children = store_1.driveStore.listItems(itemId);
50
+ let children = store_1.driveStore.listItems(itemId);
51
+ // Basic OData $filter REJECTION
52
+ if (req.query.$filter && typeof req.query.$filter === 'string') {
53
+ res.status(400).json({
54
+ error: {
55
+ code: 'invalidRequest',
56
+ message: 'Invalid request'
57
+ }
58
+ });
59
+ return;
60
+ }
61
+ // Basic OData $orderby support
62
+ if (req.query.$orderby && typeof req.query.$orderby === 'string') {
63
+ const descending = req.query.$orderby.includes('desc');
64
+ children.sort((a, b) => {
65
+ const timeA = a.lastModifiedDateTime || "";
66
+ const timeB = b.lastModifiedDateTime || "";
67
+ if (timeA === timeB) {
68
+ return a.name > b.name ? 1 : -1;
69
+ }
70
+ if (descending)
71
+ return timeA < timeB ? 1 : -1;
72
+ return timeA > timeB ? 1 : -1;
73
+ });
74
+ }
75
+ // Basic OData $top support and $skip REJECTION
76
+ let skip = 0;
77
+ if (req.query.$skip && typeof req.query.$skip === 'string') {
78
+ res.status(400).json({
79
+ error: {
80
+ code: 'invalidRequest',
81
+ message: '$skip is not supported on this API. Only URLs returned by the API can be used to page.'
82
+ }
83
+ });
84
+ return;
85
+ }
86
+ // Basic OData $top support and $skipToken pagination
87
+ if (req.query.$skipToken && typeof req.query.$skipToken === 'string') {
88
+ const parsedToken = parseInt(req.query.$skipToken, 10);
89
+ if (!isNaN(parsedToken))
90
+ skip = parsedToken;
91
+ }
92
+ let hasMore = false;
93
+ let nextSkipToken = 0;
94
+ if (req.query.$top && typeof req.query.$top === 'string') {
95
+ const top = parseInt(req.query.$top, 10);
96
+ if (!isNaN(top)) {
97
+ if (skip + top < children.length) {
98
+ hasMore = true;
99
+ nextSkipToken = skip + top;
100
+ }
101
+ children = children.slice(skip, skip + top);
102
+ }
103
+ }
104
+ else if (skip > 0) {
105
+ children = children.slice(skip);
106
+ }
51
107
  const mappedChildren = children.map(c => applySelect(c, req.query.$select));
52
- res.json({ value: mappedChildren });
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ const response = { value: mappedChildren };
110
+ if (hasMore) {
111
+ const host = req.headers.host || 'localhost';
112
+ const protocol = req.protocol || 'http';
113
+ const baseBaseUrl = `${protocol}://${host}`;
114
+ const url = new URL(req.originalUrl || req.url, baseBaseUrl);
115
+ url.searchParams.set('$skipToken', nextSkipToken.toString());
116
+ response['@odata.nextLink'] = url.toString();
117
+ }
118
+ if (req.query.$count === 'true') {
119
+ res.status(400).json({
120
+ error: {
121
+ code: 'invalidRequest',
122
+ message: '$count is not supported on this API. Only URLs returned by the API can be used to page.'
123
+ }
124
+ });
125
+ return;
126
+ }
127
+ res.json(response);
53
128
  });
54
129
  // GET /me/drive/root/search(q='query')
55
130
  app.get('/v1.0/me/drive/root/search\\(q=\':query\'\\)', (req, res) => {
@@ -96,12 +171,33 @@ const createV1Router = () => {
96
171
  }
97
172
  const ifMatch = req.header('If-Match');
98
173
  if (ifMatch && ifMatch !== item.eTag) {
99
- res.status(412).json({ error: { code: "PreconditionFailed", message: "ETag mismatch" } });
174
+ res.status(409).json({ error: { code: "preconditionFailed", message: "ETag mismatch" } });
100
175
  return;
101
176
  }
102
177
  store_1.driveStore.deleteItem(fileId);
103
178
  res.status(204).send();
104
179
  });
180
+ // PATCH /me/drive/items/{id}
181
+ app.patch('/v1.0/me/drive/items/:itemId', (req, res) => {
182
+ const fileId = req.params.itemId;
183
+ if (fileId === 'root') {
184
+ res.status(400).json({ error: { message: "Cannot modify root" } });
185
+ return;
186
+ }
187
+ const item = store_1.driveStore.getItem(fileId);
188
+ if (!item) {
189
+ res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
190
+ return;
191
+ }
192
+ const ifMatch = req.header('If-Match');
193
+ if (ifMatch && ifMatch !== item.eTag) {
194
+ res.status(409).json({ error: { code: "preconditionFailed", message: "ETag mismatch" } });
195
+ return;
196
+ }
197
+ const updates = req.body;
198
+ const updatedItem = store_1.driveStore.updateItem(fileId, updates);
199
+ res.status(200).json(applySelect(updatedItem, req.query.$select));
200
+ });
105
201
  // GET /me/drive/items/{id}/content
106
202
  app.get('/v1.0/me/drive/items/:itemId/content', (req, res) => {
107
203
  var _a;
@@ -128,6 +224,36 @@ const createV1Router = () => {
128
224
  res.send(file.content);
129
225
  }
130
226
  });
227
+ // Path-based Addressing
228
+ app.get('/v1.0/me/drive/items/:parentId\\:/:filename', (req, res) => {
229
+ const params = req.params;
230
+ const parentId = params.parentId;
231
+ let filename = params.filename;
232
+ if (filename.endsWith(':'))
233
+ filename = filename.slice(0, -1);
234
+ filename = decodeURIComponent(filename);
235
+ const parentObj = store_1.driveStore.getItem(parentId) || (parentId === 'root' ? store_1.driveStore.getItem('root') : null);
236
+ if (!parentObj) {
237
+ return res.status(404).json({ error: { code: "itemNotFound", message: "Parent not found" } });
238
+ }
239
+ const child = store_1.driveStore.getItemByName(parentId, filename);
240
+ if (!child) {
241
+ return res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
242
+ }
243
+ res.status(200).json(applySelect(child, req.query.$select));
244
+ });
245
+ app.get('/v1.0/me/drive/root\\:/:filename', (req, res) => {
246
+ const params = req.params;
247
+ let filename = params.filename;
248
+ if (filename.endsWith(':'))
249
+ filename = filename.slice(0, -1);
250
+ filename = decodeURIComponent(filename);
251
+ const child = store_1.driveStore.getItemByName('root', filename);
252
+ if (!child) {
253
+ return res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
254
+ }
255
+ res.status(200).json(applySelect(child, req.query.$select));
256
+ });
131
257
  // PUT /me/drive/items/{parent-id}:/{filename}:/content
132
258
  app.put('/v1.0/me/drive/items/:parentId\\:/:filename\\:/content', (req, res) => {
133
259
  const parentId = req.params.parentId;
@@ -146,7 +272,8 @@ const createV1Router = () => {
146
272
  return;
147
273
  }
148
274
  // Update
149
- item = store_1.driveStore.updateItem(item.id, { content, file: { mimeType } });
275
+ const size = content ? (Buffer.isBuffer(content) || typeof content === 'string' ? content.length : JSON.stringify(content).length) : 0;
276
+ item = store_1.driveStore.updateItem(item.id, { content, file: { mimeType }, size });
150
277
  }
151
278
  else {
152
279
  // Create
@@ -154,7 +281,8 @@ const createV1Router = () => {
154
281
  name: filename,
155
282
  parentReference: { id: parentId },
156
283
  content,
157
- file: { mimeType }
284
+ file: { mimeType },
285
+ size: content ? (Buffer.isBuffer(content) || typeof content === 'string' ? content.length : JSON.stringify(content).length) : 0
158
286
  });
159
287
  }
160
288
  res.status(isNew ? 201 : 200).json(applySelect(item, req.query.$select));
@@ -176,7 +304,8 @@ const createV1Router = () => {
176
304
  const content = req.rawBody !== undefined ? req.rawBody : req.body;
177
305
  const headerMime = req.headers['content-type'];
178
306
  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 } });
307
+ const size = content ? (Buffer.isBuffer(content) || typeof content === 'string' ? content.length : JSON.stringify(content).length) : 0;
308
+ item = store_1.driveStore.updateItem(item.id, { content, file: { mimeType }, size });
180
309
  res.status(200).json(applySelect(item, req.query.$select));
181
310
  });
182
311
  // Delta Query
@@ -196,6 +325,133 @@ const createV1Router = () => {
196
325
  value: result.items
197
326
  });
198
327
  });
328
+ // ==========================================
329
+ // MISSING ENDPOINTS IMPLEMENTATION
330
+ // ==========================================
331
+ // --- Drives & Shared Content ---
332
+ const defaultDrive = {
333
+ id: "b!default-mock-drive-id",
334
+ driveType: "personal",
335
+ name: "OneDrive",
336
+ owner: { user: { id: "user1", displayName: "Mock User" } }
337
+ };
338
+ app.get('/v1.0/me/drives', (req, res) => {
339
+ res.json({ value: [defaultDrive] });
340
+ });
341
+ app.get('/v1.0/drives/:driveId', (req, res) => {
342
+ res.json(defaultDrive);
343
+ });
344
+ app.get('/v1.0/me/drive/sharedWithMe', (req, res) => {
345
+ res.json({ value: [] });
346
+ });
347
+ app.get('/v1.0/me/drive/recent', (req, res) => {
348
+ res.json({ value: [] });
349
+ });
350
+ app.get('/v1.0/me/drive/following', (req, res) => {
351
+ res.json({ value: [] });
352
+ });
353
+ // --- Special Folders ---
354
+ app.get('/v1.0/me/drive/special/:folderName', (req, res) => {
355
+ const root = store_1.driveStore.getItem('root');
356
+ if (!root)
357
+ return res.status(404).json({ error: { message: "Not found" } });
358
+ res.json(root);
359
+ });
360
+ // --- Advanced Item Operations ---
361
+ app.post('/v1.0/me/drive/items/:itemId/copy', (req, res) => {
362
+ const itemId = req.params.itemId;
363
+ const item = store_1.driveStore.getItem(itemId);
364
+ if (!item)
365
+ return res.status(404).json({ error: { message: "Not found" } });
366
+ const host = req.headers.host || 'localhost';
367
+ const protocol = req.protocol || 'http';
368
+ const baseUrl = `${protocol}://${host}`;
369
+ res.setHeader('Location', `${baseUrl}/v1.0/monitor/mock-copy-job-12345`);
370
+ res.status(202).json({});
371
+ });
372
+ app.post('/v1.0/me/drive/items/:itemId/createLink', (req, res) => {
373
+ const item = store_1.driveStore.getItem(req.params.itemId);
374
+ if (!item)
375
+ return res.status(404).json({ error: { message: "Not found" } });
376
+ res.json({
377
+ id: "mock-link-id",
378
+ roles: ["write"],
379
+ link: { webUrl: "https://mock-onedrive-link/123" }
380
+ });
381
+ });
382
+ app.get('/v1.0/me/drive/items/:itemId/permissions', (req, res) => {
383
+ res.json({ value: [{ id: "perm1", roles: ["write"] }] });
384
+ });
385
+ app.post('/v1.0/me/drive/items/:itemId/invite', (req, res) => {
386
+ res.json({ value: [{ id: "perm1", roles: ["write"] }] });
387
+ });
388
+ app.delete('/v1.0/me/drive/items/:itemId/permissions/:permId', (req, res) => {
389
+ res.status(204).send();
390
+ });
391
+ app.get('/v1.0/me/drive/items/:itemId/versions', (req, res) => {
392
+ res.json({ value: [] });
393
+ });
394
+ app.post('/v1.0/me/drive/items/:itemId/versions/:versionId/restoreVersion', (req, res) => {
395
+ res.status(204).send();
396
+ });
397
+ app.post('/v1.0/me/drive/items/:itemId/checkout', (req, res) => {
398
+ res.status(204).send();
399
+ });
400
+ app.post('/v1.0/me/drive/items/:itemId/checkin', (req, res) => {
401
+ res.status(204).send();
402
+ });
403
+ app.get('/v1.0/me/drive/items/:itemId/thumbnails', (req, res) => {
404
+ res.json({
405
+ value: [
406
+ { id: "0", large: { url: "https://mock-thumbnail-url/large" } }
407
+ ]
408
+ });
409
+ });
410
+ app.get('/v1.0/me/drive/items/:itemId/activities', (req, res) => {
411
+ res.json({ value: [] });
412
+ });
413
+ // --- Upload Sessions ---
414
+ const handleCreateUploadSession = (parentId, filename, req, res) => {
415
+ const parentObj = store_1.driveStore.getItem(parentId) || (parentId === 'root' ? store_1.driveStore.getItem('root') : null);
416
+ if (!parentObj) {
417
+ return res.status(404).json({ error: { code: "itemNotFound", message: "Parent not found" } });
418
+ }
419
+ const session = store_1.driveStore.createUploadSession(parentId, filename);
420
+ const host = req.headers.host || 'localhost';
421
+ const protocol = req.protocol || 'http';
422
+ const baseUrl = `${protocol}://${host}`;
423
+ res.status(200).json({
424
+ uploadUrl: `${baseUrl}/v1.0${session.uploadUrl}`,
425
+ expirationDateTime: session.expirationDateTime
426
+ });
427
+ };
428
+ app.post('/v1.0/me/drive/items/:parentId\\:/:filename\\:/createUploadSession', (req, res) => {
429
+ const params = req.params;
430
+ handleCreateUploadSession(params.parentId, decodeURIComponent(params.filename), req, res);
431
+ });
432
+ app.post('/v1.0/me/drive/root\\:/:filename\\:/createUploadSession', (req, res) => {
433
+ const params = req.params;
434
+ handleCreateUploadSession('root', decodeURIComponent(params.filename), req, res);
435
+ });
436
+ // --- Subscriptions ---
437
+ app.post('/v1.0/subscriptions', (req, res) => {
438
+ res.status(201).json({
439
+ id: "mock-subscription-123",
440
+ expirationDateTime: new Date(Date.now() + 2 * 24 * 3600 * 1000).toISOString(),
441
+ clientState: req.body.clientState || "mock-secret"
442
+ });
443
+ });
444
+ app.get('/v1.0/subscriptions', (req, res) => {
445
+ res.json({ value: [] });
446
+ });
447
+ app.put('/v1.0/upload-sessions/:sessionId', express_1.default.raw({ type: '*/*', limit: '50mb' }), (req, res) => {
448
+ const sessionId = req.params.sessionId;
449
+ const session = store_1.driveStore.getUploadSession(sessionId);
450
+ if (!session)
451
+ return res.status(404).json({ error: { message: "Session not found" } });
452
+ const item = store_1.driveStore.completeUploadSession(sessionId);
453
+ res.status(200).json(item);
454
+ });
199
455
  return app;
200
456
  };
201
457
  exports.createV1Router = createV1Router;
package/dist/store.d.ts CHANGED
@@ -2,6 +2,7 @@ import { DriveItem } from './types';
2
2
  export declare class DriveStore {
3
3
  private items;
4
4
  private deltaHistory;
5
+ private uploadSessions;
5
6
  constructor();
6
7
  private calculateStats;
7
8
  createItem(item: Partial<DriveItem> & {
@@ -9,10 +10,22 @@ export declare class DriveStore {
9
10
  }, isFolder?: boolean): DriveItem;
10
11
  updateItem(id: string, updates: Partial<DriveItem>): DriveItem | null;
11
12
  getItem(id: string): DriveItem | null;
13
+ getItemByName(parentId: string, name: string): DriveItem | null;
12
14
  deleteItem(id: string): boolean;
13
15
  listItems(parentId?: string): DriveItem[];
14
16
  getAllItems(): DriveItem[];
15
17
  clear(): void;
18
+ createUploadSession(parentId: string, filename: string): {
19
+ uploadUrl: string;
20
+ expirationDateTime: string;
21
+ };
22
+ completeUploadSession(sessionId: string): DriveItem | null;
23
+ getUploadSession(sessionId: string): {
24
+ parentId: string;
25
+ filename: string;
26
+ expirationDateTime: string;
27
+ } | null;
28
+ deleteUploadSession(sessionId: string): void;
16
29
  private addDeltaHistory;
17
30
  getDeltaToken(): string;
18
31
  getDelta(token?: string): {
package/dist/store.js CHANGED
@@ -39,6 +39,7 @@ class DriveStore {
39
39
  constructor() {
40
40
  this.items = new Map();
41
41
  this.deltaHistory = [];
42
+ this.uploadSessions = new Map();
42
43
  }
43
44
  calculateStats(content) {
44
45
  let buffer;
@@ -107,6 +108,10 @@ class DriveStore {
107
108
  getItem(id) {
108
109
  return this.items.get(id) || null;
109
110
  }
111
+ getItemByName(parentId, name) {
112
+ const children = this.listItems(parentId);
113
+ return children.find(c => c.name === name && !c.deleted) || null;
114
+ }
110
115
  deleteItem(id) {
111
116
  const item = this.items.get(id);
112
117
  if (!item)
@@ -134,9 +139,39 @@ class DriveStore {
134
139
  clear() {
135
140
  this.items.clear();
136
141
  this.deltaHistory = [];
142
+ this.uploadSessions.clear();
137
143
  // Always recreate a standard root folder
138
144
  this.createItem({ id: 'root', name: 'root' }, true);
139
145
  }
146
+ // Upload Sessions
147
+ createUploadSession(parentId, filename) {
148
+ const sessionId = Math.random().toString(36).substring(7);
149
+ const expirationDateTime = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // 1 day
150
+ this.uploadSessions.set(sessionId, { parentId, filename, expirationDateTime });
151
+ // Return a relative URL path to be prefixed by the router
152
+ return {
153
+ uploadUrl: `/upload-sessions/${sessionId}`,
154
+ expirationDateTime
155
+ };
156
+ }
157
+ completeUploadSession(sessionId) {
158
+ const session = this.uploadSessions.get(sessionId);
159
+ if (!session)
160
+ return null;
161
+ this.uploadSessions.delete(sessionId);
162
+ const item = this.createItem({
163
+ name: session.filename,
164
+ file: { mimeType: 'application/octet-stream' },
165
+ parentReference: { id: session.parentId }
166
+ });
167
+ return item;
168
+ }
169
+ getUploadSession(sessionId) {
170
+ return this.uploadSessions.get(sessionId) || null;
171
+ }
172
+ deleteUploadSession(sessionId) {
173
+ this.uploadSessions.delete(sessionId);
174
+ }
140
175
  // Delta History (simulated changes API)
141
176
  addDeltaHistory(item) {
142
177
  this.deltaHistory.push(JSON.parse(JSON.stringify(item)));
@@ -145,6 +180,12 @@ class DriveStore {
145
180
  return String(this.deltaHistory.length);
146
181
  }
147
182
  getDelta(token) {
183
+ if (token === 'latest') {
184
+ return {
185
+ items: [],
186
+ deltaLink: String(this.deltaHistory.length)
187
+ };
188
+ }
148
189
  const tokenIndex = token ? parseInt(token, 10) : 0;
149
190
  const start = isNaN(tokenIndex) ? 0 : Math.max(0, tokenIndex);
150
191
  const items = this.deltaHistory.slice(start);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "microsoft-onedrive-mock",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Mock-Server that simulates being Microsoft OneDrive. Used for testing.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",