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 +16 -0
- package/.github/workflows/ci.yml +41 -0
- package/.github/workflows/release.yml +50 -0
- package/README.md +1 -0
- package/dist/batch.d.ts +2 -0
- package/dist/batch.js +93 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +115 -0
- package/dist/routes/v1.d.ts +1 -0
- package/dist/routes/v1.js +201 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.js +175 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +2 -0
- package/eslint.config.mjs +15 -0
- package/examples/device-login.ts +84 -0
- package/examples/microsoft-login.html +227 -0
- package/examples/serve-login.ts +11 -0
- package/package.json +48 -0
- package/scripts/check-token.ts +70 -0
- package/specs/odata.xml +2 -0
- package/specs/openapi.yaml +196491 -0
- package/src/batch.ts +91 -0
- package/src/index.ts +102 -0
- package/src/routes/v1.ts +227 -0
- package/src/store.ts +191 -0
- package/src/types.ts +59 -0
- package/test/basics.test.ts +119 -0
- package/test/batch.test.ts +75 -0
- package/test/config.ts +63 -0
- package/test/etag.test.ts +69 -0
- package/test/search.test.ts +57 -0
- package/test/select.test.ts +68 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +33 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.driveStore = exports.DriveStore = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
class DriveStore {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.items = new Map();
|
|
41
|
+
this.deltaHistory = [];
|
|
42
|
+
}
|
|
43
|
+
calculateStats(content) {
|
|
44
|
+
let buffer;
|
|
45
|
+
if (typeof content === 'string') {
|
|
46
|
+
buffer = Buffer.from(content);
|
|
47
|
+
}
|
|
48
|
+
else if (Buffer.isBuffer(content)) {
|
|
49
|
+
buffer = content;
|
|
50
|
+
}
|
|
51
|
+
else if (content === undefined || content === null) {
|
|
52
|
+
buffer = Buffer.from('');
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
buffer = Buffer.from(JSON.stringify(content));
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
size: buffer.length,
|
|
59
|
+
sha1Hash: crypto.createHash('sha1').update(buffer).digest('hex')
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
createItem(item, isFolder = false) {
|
|
63
|
+
var _a;
|
|
64
|
+
if (!item.name) {
|
|
65
|
+
throw new Error("Item name is required");
|
|
66
|
+
}
|
|
67
|
+
const id = item.id || Math.random().toString(36).substring(7);
|
|
68
|
+
const now = new Date().toISOString();
|
|
69
|
+
const newItem = Object.assign(Object.assign({ createdDateTime: now, lastModifiedDateTime: now }, item), { id, name: item.name, eTag: `W/"1"`, cTag: `"cTag-1"`, size: 0, parentReference: item.parentReference || { driveId: "b!", id: "root" } });
|
|
70
|
+
if (isFolder) {
|
|
71
|
+
newItem.folder = { childCount: 0 };
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const stats = this.calculateStats(item.content);
|
|
75
|
+
newItem.size = stats.size;
|
|
76
|
+
newItem.file = {
|
|
77
|
+
mimeType: ((_a = item.file) === null || _a === void 0 ? void 0 : _a.mimeType) || "application/octet-stream",
|
|
78
|
+
hashes: { sha1Hash: stats.sha1Hash }
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
this.items.set(id, newItem);
|
|
82
|
+
this.addDeltaHistory(newItem);
|
|
83
|
+
// Update parent childCount if making a folder
|
|
84
|
+
if (newItem.parentReference && newItem.parentReference.id) {
|
|
85
|
+
this.incrementParentChildCount(newItem.parentReference.id, 1);
|
|
86
|
+
}
|
|
87
|
+
return newItem;
|
|
88
|
+
}
|
|
89
|
+
updateItem(id, updates) {
|
|
90
|
+
const item = this.items.get(id);
|
|
91
|
+
if (!item)
|
|
92
|
+
return null;
|
|
93
|
+
// Extract internal version number from etag to increment
|
|
94
|
+
const currentVersion = parseInt(item.eTag.replace(/\D/g, '') || "1", 10);
|
|
95
|
+
const newVersion = currentVersion + 1;
|
|
96
|
+
const statsUpdates = {};
|
|
97
|
+
if (updates.content !== undefined && item.file) {
|
|
98
|
+
const stats = this.calculateStats(updates.content);
|
|
99
|
+
statsUpdates.size = stats.size;
|
|
100
|
+
statsUpdates.file = Object.assign(Object.assign({}, item.file), { hashes: { sha1Hash: stats.sha1Hash } });
|
|
101
|
+
}
|
|
102
|
+
const updatedItem = Object.assign(Object.assign(Object.assign(Object.assign({}, item), updates), statsUpdates), { eTag: `W/"${newVersion}"`, cTag: `"cTag-${newVersion}"`, lastModifiedDateTime: updates.lastModifiedDateTime || new Date().toISOString() });
|
|
103
|
+
this.items.set(id, updatedItem);
|
|
104
|
+
this.addDeltaHistory(updatedItem);
|
|
105
|
+
return updatedItem;
|
|
106
|
+
}
|
|
107
|
+
getItem(id) {
|
|
108
|
+
return this.items.get(id) || null;
|
|
109
|
+
}
|
|
110
|
+
deleteItem(id) {
|
|
111
|
+
const item = this.items.get(id);
|
|
112
|
+
if (!item)
|
|
113
|
+
return false;
|
|
114
|
+
const deleted = this.items.delete(id);
|
|
115
|
+
if (deleted) {
|
|
116
|
+
const deletedItem = Object.assign(Object.assign({}, item), { deleted: { state: "deleted" }, lastModifiedDateTime: new Date().toISOString() });
|
|
117
|
+
this.addDeltaHistory(deletedItem);
|
|
118
|
+
// Decrement parent count
|
|
119
|
+
if (item.parentReference && item.parentReference.id) {
|
|
120
|
+
this.incrementParentChildCount(item.parentReference.id, -1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return deleted;
|
|
124
|
+
}
|
|
125
|
+
listItems(parentId) {
|
|
126
|
+
const allItems = Array.from(this.items.values());
|
|
127
|
+
if (!parentId)
|
|
128
|
+
return allItems;
|
|
129
|
+
return allItems.filter(i => { var _a; return ((_a = i.parentReference) === null || _a === void 0 ? void 0 : _a.id) === parentId; });
|
|
130
|
+
}
|
|
131
|
+
getAllItems() {
|
|
132
|
+
return Array.from(this.items.values());
|
|
133
|
+
}
|
|
134
|
+
clear() {
|
|
135
|
+
this.items.clear();
|
|
136
|
+
this.deltaHistory = [];
|
|
137
|
+
// Always recreate a standard root folder
|
|
138
|
+
this.createItem({ id: 'root', name: 'root' }, true);
|
|
139
|
+
}
|
|
140
|
+
// Delta History (simulated changes API)
|
|
141
|
+
addDeltaHistory(item) {
|
|
142
|
+
this.deltaHistory.push(JSON.parse(JSON.stringify(item)));
|
|
143
|
+
}
|
|
144
|
+
getDeltaToken() {
|
|
145
|
+
return String(this.deltaHistory.length);
|
|
146
|
+
}
|
|
147
|
+
getDelta(token) {
|
|
148
|
+
const tokenIndex = token ? parseInt(token, 10) : 0;
|
|
149
|
+
const start = isNaN(tokenIndex) ? 0 : Math.max(0, tokenIndex);
|
|
150
|
+
const items = this.deltaHistory.slice(start);
|
|
151
|
+
// In MS Graph, if a file transitions multiple states, delta should ideally just return the latest state
|
|
152
|
+
// but for a mock, returning the log or deduping by id to latest state is standard.
|
|
153
|
+
// Let's dedupe to match real API behavior mostly (returns latest state within the page)
|
|
154
|
+
const dedupedMap = new Map();
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
dedupedMap.set(item.id, item);
|
|
157
|
+
}
|
|
158
|
+
const dedupedItems = Array.from(dedupedMap.values());
|
|
159
|
+
const newToken = String(this.deltaHistory.length);
|
|
160
|
+
return {
|
|
161
|
+
items: dedupedItems,
|
|
162
|
+
deltaLink: newToken
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
incrementParentChildCount(parentId, amount) {
|
|
166
|
+
const parent = this.items.get(parentId);
|
|
167
|
+
if (parent && parent.folder) {
|
|
168
|
+
parent.folder.childCount = Math.max(0, parent.folder.childCount + amount);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.DriveStore = DriveStore;
|
|
173
|
+
exports.driveStore = new DriveStore();
|
|
174
|
+
// Initialize root on boot
|
|
175
|
+
exports.driveStore.clear();
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface AppConfig {
|
|
2
|
+
apiEndpoint?: string;
|
|
3
|
+
serverLagBefore?: number;
|
|
4
|
+
serverLagAfter?: number;
|
|
5
|
+
}
|
|
6
|
+
declare global {
|
|
7
|
+
namespace Express {
|
|
8
|
+
interface Request {
|
|
9
|
+
rawBody?: Buffer | string;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export interface DriveItem {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
eTag: string;
|
|
17
|
+
cTag: string;
|
|
18
|
+
createdBy?: {
|
|
19
|
+
user: {
|
|
20
|
+
displayName: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
lastModifiedBy?: {
|
|
24
|
+
user: {
|
|
25
|
+
displayName: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
createdDateTime: string;
|
|
29
|
+
lastModifiedDateTime: string;
|
|
30
|
+
size: number;
|
|
31
|
+
parentReference?: {
|
|
32
|
+
driveId?: string;
|
|
33
|
+
driveType?: string;
|
|
34
|
+
id?: string;
|
|
35
|
+
path?: string;
|
|
36
|
+
};
|
|
37
|
+
file?: {
|
|
38
|
+
mimeType: string;
|
|
39
|
+
hashes?: {
|
|
40
|
+
quickXorHash?: string;
|
|
41
|
+
sha1Hash?: string;
|
|
42
|
+
sha256Hash?: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
folder?: {
|
|
46
|
+
childCount: number;
|
|
47
|
+
};
|
|
48
|
+
deleted?: {
|
|
49
|
+
state: string;
|
|
50
|
+
};
|
|
51
|
+
'@microsoft.graph.downloadUrl'?: string;
|
|
52
|
+
content?: unknown;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
export interface DeltaResponse {
|
|
56
|
+
'@odata.context': string;
|
|
57
|
+
'@odata.nextLink'?: string;
|
|
58
|
+
'@odata.deltaLink'?: string;
|
|
59
|
+
value: DriveItem[];
|
|
60
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import pluginJs from "@eslint/js";
|
|
3
|
+
import tseslint from "typescript-eslint";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/** @type {import('eslint').Linter.Config[]} */
|
|
7
|
+
export default [
|
|
8
|
+
{ files: ["**/*.{js,mjs,cjs,ts}"] },
|
|
9
|
+
{ languageOptions: { globals: globals.node } },
|
|
10
|
+
pluginJs.configs.recommended,
|
|
11
|
+
...tseslint.configs.recommended,
|
|
12
|
+
{
|
|
13
|
+
ignores: ["dist/**", "coverage/**", "node_modules/**"]
|
|
14
|
+
}
|
|
15
|
+
];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
const clientId = 'e5f346a8-8996-4d46-9f93-6e6817d9078e';
|
|
5
|
+
const tenantId = 'common';
|
|
6
|
+
|
|
7
|
+
async function runDeviceLogin() {
|
|
8
|
+
const authority = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0`;
|
|
9
|
+
|
|
10
|
+
// 1. Get device code
|
|
11
|
+
const deviceCodeParams = new URLSearchParams();
|
|
12
|
+
deviceCodeParams.append('client_id', clientId);
|
|
13
|
+
deviceCodeParams.append('scope', 'Files.ReadWrite.All User.Read offline_access');
|
|
14
|
+
|
|
15
|
+
let deviceCodeRes;
|
|
16
|
+
try {
|
|
17
|
+
deviceCodeRes = await fetch(`${authority}/devicecode`, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
21
|
+
},
|
|
22
|
+
body: deviceCodeParams.toString()
|
|
23
|
+
});
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error("Network error executing fetch request:", e);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const deviceCodeData = await deviceCodeRes.json();
|
|
30
|
+
if (!deviceCodeRes.ok) {
|
|
31
|
+
console.error('❌ Failed to start device login:', deviceCodeData);
|
|
32
|
+
console.error('\nNOTE: Your Azure App must have "Allow public client flows" enabled in Authentication settings for Device Code flow to work.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log('\n====================================================');
|
|
37
|
+
console.log(deviceCodeData.message);
|
|
38
|
+
console.log('====================================================\n');
|
|
39
|
+
|
|
40
|
+
// 2. Poll for token
|
|
41
|
+
const tokenParams = new URLSearchParams();
|
|
42
|
+
tokenParams.append('grant_type', 'urn:ietf:params:oauth:grant-type:device_code');
|
|
43
|
+
tokenParams.append('client_id', clientId);
|
|
44
|
+
tokenParams.append('device_code', deviceCodeData.device_code);
|
|
45
|
+
|
|
46
|
+
let polling = true;
|
|
47
|
+
while (polling) {
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, deviceCodeData.interval * 1000));
|
|
49
|
+
|
|
50
|
+
const tokenRes = await fetch(`${authority}/token`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
54
|
+
},
|
|
55
|
+
body: tokenParams.toString()
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const tokenData = await tokenRes.json();
|
|
59
|
+
|
|
60
|
+
if (tokenRes.ok) {
|
|
61
|
+
console.log('\n✅ Successfully authenticated!');
|
|
62
|
+
|
|
63
|
+
// Save to .ENV
|
|
64
|
+
const envPath = path.resolve(__dirname, '../.ENV');
|
|
65
|
+
let envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
|
|
66
|
+
if (envContent.includes('ONEDRIVE_TOKEN=')) {
|
|
67
|
+
envContent = envContent.replace(/ONEDRIVE_TOKEN=[^\n]*/, `ONEDRIVE_TOKEN=${tokenData.access_token}`);
|
|
68
|
+
} else {
|
|
69
|
+
envContent += `\nONEDRIVE_TOKEN=${tokenData.access_token}\n`;
|
|
70
|
+
}
|
|
71
|
+
fs.writeFileSync(envPath, envContent);
|
|
72
|
+
console.log('✅ Updated .ENV with your new ONEDRIVE_TOKEN');
|
|
73
|
+
console.log('You can now run: npm run test:real');
|
|
74
|
+
polling = false;
|
|
75
|
+
} else if (tokenData.error !== 'authorization_pending') {
|
|
76
|
+
console.error('\n❌ Login failed:', tokenData.error_description || tokenData.error);
|
|
77
|
+
polling = false;
|
|
78
|
+
} else {
|
|
79
|
+
process.stdout.write('.');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
runDeviceLogin();
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Microsoft OneDrive Token Generator</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: sans-serif;
|
|
11
|
+
padding: 20px;
|
|
12
|
+
max-width: 600px;
|
|
13
|
+
margin: 0 auto;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.form-group {
|
|
17
|
+
margin-bottom: 15px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
label {
|
|
21
|
+
display: block;
|
|
22
|
+
margin-bottom: 5px;
|
|
23
|
+
font-weight: bold;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
input[type="text"] {
|
|
27
|
+
width: 100%;
|
|
28
|
+
padding: 8px;
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
button {
|
|
33
|
+
padding: 10px 20px;
|
|
34
|
+
background-color: #0078d4;
|
|
35
|
+
color: white;
|
|
36
|
+
border: none;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
font-size: 16px;
|
|
39
|
+
border-radius: 4px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
button:hover {
|
|
43
|
+
background-color: #005a9e;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
button#copy-btn {
|
|
47
|
+
background-color: #28a745;
|
|
48
|
+
margin-top: 10px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
button#copy-btn:hover {
|
|
52
|
+
background-color: #218838;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#token-display {
|
|
56
|
+
margin-top: 20px;
|
|
57
|
+
background: #f0f0f0;
|
|
58
|
+
padding: 15px;
|
|
59
|
+
border-radius: 4px;
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
textarea {
|
|
64
|
+
width: 100%;
|
|
65
|
+
padding: 8px;
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
border: 1px solid #ccc;
|
|
68
|
+
border-radius: 4px;
|
|
69
|
+
resize: vertical;
|
|
70
|
+
font-family: monospace;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.hidden {
|
|
74
|
+
display: none;
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
77
|
+
<!-- MSAL.js v3 -->
|
|
78
|
+
<script type="text/javascript" src="https://alcdn.msauth.net/browser/2.37.1/js/msal-browser.min.js"></script>
|
|
79
|
+
</head>
|
|
80
|
+
|
|
81
|
+
<body>
|
|
82
|
+
|
|
83
|
+
<h1>Microsoft OneDrive Login</h1>
|
|
84
|
+
<p>Enter your Client ID (Application ID) and Tenant ID to authenticate and get an access token. The login uses
|
|
85
|
+
MSAL.js for OAuth2 Implicit/SPA flow.</p>
|
|
86
|
+
|
|
87
|
+
<div class="form-group">
|
|
88
|
+
<label for="client-id">Client ID:</label>
|
|
89
|
+
<input type="text" id="client-id" placeholder="e.g. 11111111-1111-1111-1111-111111111111">
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="form-group">
|
|
93
|
+
<label for="tenant-id">Tenant ID:</label>
|
|
94
|
+
<input type="text" id="tenant-id" placeholder="e.g. common (or your specific tenant GUID)">
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<button id="login-btn" onclick="handleAuth()">Login to Microsoft Graph</button>
|
|
98
|
+
|
|
99
|
+
<div id="token-display">
|
|
100
|
+
<h3>Access Token:</h3>
|
|
101
|
+
<textarea id="access-token" readonly rows="6"></textarea>
|
|
102
|
+
<button id="copy-btn" onclick="copyToken()">Copy Token</button>
|
|
103
|
+
|
|
104
|
+
<h3>Full .ENV Content:</h3>
|
|
105
|
+
<textarea id="env-content" readonly rows="12"></textarea>
|
|
106
|
+
<button id="copy-env-btn" onclick="copyEnv()">Copy .ENV</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<script>
|
|
110
|
+
let msalInstance = null;
|
|
111
|
+
let loginRequest = {
|
|
112
|
+
scopes: ["Files.ReadWrite.All", "User.Read"]
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
window.onload = async function () {
|
|
116
|
+
const storedClientId = localStorage.getItem('ms_client_id');
|
|
117
|
+
const storedTenantId = localStorage.getItem('ms_tenant_id') || 'common';
|
|
118
|
+
if (storedClientId) document.getElementById('client-id').value = storedClientId;
|
|
119
|
+
document.getElementById('tenant-id').value = storedTenantId;
|
|
120
|
+
|
|
121
|
+
// If we have clientId, we can initialize MSAL to check if we are returning from a redirect
|
|
122
|
+
if (storedClientId) {
|
|
123
|
+
initializeMsal(storedClientId, storedTenantId);
|
|
124
|
+
try {
|
|
125
|
+
const response = await msalInstance.handleRedirectPromise();
|
|
126
|
+
if (response && response.accessToken) {
|
|
127
|
+
displayToken(response.accessToken, storedClientId, storedTenantId);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error("Redirect error:", err);
|
|
131
|
+
alert("Failed to process login redirect: " + err.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function initializeMsal(clientId, tenantId) {
|
|
137
|
+
// Determine if the user is running on https or http
|
|
138
|
+
const currentOrigin = window.location.origin;
|
|
139
|
+
const redirectUri = currentOrigin + "/microsoft-login.html";
|
|
140
|
+
|
|
141
|
+
const msalConfig = {
|
|
142
|
+
auth: {
|
|
143
|
+
clientId: clientId,
|
|
144
|
+
authority: `https://login.microsoftonline.com/${tenantId}`,
|
|
145
|
+
redirectUri: redirectUri,
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
msalInstance = new msal.PublicClientApplication(msalConfig);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function handleAuth() {
|
|
152
|
+
const clientId = document.getElementById('client-id').value.trim();
|
|
153
|
+
const tenantId = document.getElementById('tenant-id').value.trim() || 'common';
|
|
154
|
+
|
|
155
|
+
if (!clientId) {
|
|
156
|
+
alert('Please enter a Client ID');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
localStorage.setItem('ms_client_id', clientId);
|
|
161
|
+
localStorage.setItem('ms_tenant_id', tenantId);
|
|
162
|
+
|
|
163
|
+
initializeMsal(clientId, tenantId);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
// Use redirect flow instead of popup. This avoids popup blockers and
|
|
167
|
+
// cleanly returns the user to this page with the '#code=...' in the URL,
|
|
168
|
+
// which handleRedirectPromise() will then automatically exchange for the JWT Access Token!
|
|
169
|
+
await msalInstance.loginRedirect(loginRequest);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error(err);
|
|
172
|
+
alert('Login failed: ' + (err.message || err));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function displayToken(accessToken, clientId, tenantId) {
|
|
177
|
+
document.getElementById('token-display').style.display = 'block';
|
|
178
|
+
document.getElementById('access-token').value = accessToken;
|
|
179
|
+
|
|
180
|
+
const envContent = `# To run tests against the real Microsoft Graph API
|
|
181
|
+
# Copy this file to .ENV and fill in the values
|
|
182
|
+
|
|
183
|
+
# 'mock' (default) or 'real'
|
|
184
|
+
TEST_TARGET=real
|
|
185
|
+
|
|
186
|
+
# OAuth2 Access Token for your Microsoft Account
|
|
187
|
+
# Get one via 'npm run example:login'
|
|
188
|
+
ONEDRIVE_TOKEN=${accessToken}
|
|
189
|
+
|
|
190
|
+
# Client ID for the Microsoft Login Example
|
|
191
|
+
ONEDRIVE_CLIENT_ID=${clientId}
|
|
192
|
+
ONEDRIVE_TENANT_ID=${tenantId}
|
|
193
|
+
|
|
194
|
+
# Optional: Simulate latency in ms (only works with TEST_TARGET=mock)
|
|
195
|
+
# LATENCY=50
|
|
196
|
+
`;
|
|
197
|
+
document.getElementById('env-content').value = envContent;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function copyToken() {
|
|
201
|
+
copyToClipboard('access-token', 'copy-btn');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function copyEnv() {
|
|
205
|
+
copyToClipboard('env-content', 'copy-env-btn');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function copyToClipboard(elementId, buttonId) {
|
|
209
|
+
const textEl = document.getElementById(elementId);
|
|
210
|
+
textEl.select();
|
|
211
|
+
textEl.setSelectionRange(0, 99999);
|
|
212
|
+
|
|
213
|
+
navigator.clipboard.writeText(textEl.value).then(() => {
|
|
214
|
+
const btn = document.getElementById(buttonId);
|
|
215
|
+
const originalText = btn.innerText;
|
|
216
|
+
btn.innerText = 'Copied!';
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
btn.innerText = originalText;
|
|
219
|
+
}, 2000);
|
|
220
|
+
}).catch(err => {
|
|
221
|
+
console.error('Failed to copy text: ', err);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
</script>
|
|
225
|
+
</body>
|
|
226
|
+
|
|
227
|
+
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
const app = express();
|
|
4
|
+
const port = 8080;
|
|
5
|
+
|
|
6
|
+
app.use(express.static(__dirname));
|
|
7
|
+
|
|
8
|
+
app.listen(port, () => {
|
|
9
|
+
console.log(`Login example running at http://localhost:${port}/microsoft-login.html`);
|
|
10
|
+
console.log('NOTE: Ensure "http://localhost:8080/microsoft-login.html" is added as a Redirect URI in your Microsoft Entra ID app registration (SPA).');
|
|
11
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "microsoft-onedrive-mock",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Mock-Server that simulates being Microsoft OneDrive. Used for testing.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"dev": "ts-node src/index.ts",
|
|
11
|
+
"test": "TEST_TARGET=mock vitest run",
|
|
12
|
+
"test:slow": "LATENCY=20 vitest run",
|
|
13
|
+
"test:browser": "PORT=3006 start-server-and-test dev http://localhost:3006 'TEST_TARGET=mock BROWSER_ENABLED=true vitest run --browser --no-file-parallelism'",
|
|
14
|
+
"test:browser:real": "TEST_TARGET=real BROWSER_ENABLED=true vitest run --browser",
|
|
15
|
+
"test:real": "ts-node scripts/check-token.ts && TEST_TARGET=real vitest run",
|
|
16
|
+
"example:login": "ts-node examples/serve-login.ts",
|
|
17
|
+
"example:device-login": "ts-node examples/device-login.ts",
|
|
18
|
+
"lint": "eslint .",
|
|
19
|
+
"lint:fix": "eslint . --fix"
|
|
20
|
+
},
|
|
21
|
+
"type": "commonjs",
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"express": "5.2.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@eslint/js": "9.39.2",
|
|
27
|
+
"@microsoft/microsoft-graph-types": "^2.43.1",
|
|
28
|
+
"@types/cors": "2.8.19",
|
|
29
|
+
"@types/express": "5.0.6",
|
|
30
|
+
"@types/node": "25.1.0",
|
|
31
|
+
"@types/supertest": "6.0.3",
|
|
32
|
+
"@vitest/browser": "4.0.18",
|
|
33
|
+
"@vitest/browser-playwright": "4.0.18",
|
|
34
|
+
"async-test-util": "2.5.0",
|
|
35
|
+
"cors": "2.8.6",
|
|
36
|
+
"dotenv": "17.2.3",
|
|
37
|
+
"eslint": "9.39.2",
|
|
38
|
+
"express": "5.2.1",
|
|
39
|
+
"globals": "17.2.0",
|
|
40
|
+
"playwright": "1.58.0",
|
|
41
|
+
"start-server-and-test": "2.1.3",
|
|
42
|
+
"supertest": "7.2.2",
|
|
43
|
+
"ts-node": "^10.9.2",
|
|
44
|
+
"typescript": "5.9.3",
|
|
45
|
+
"typescript-eslint": "8.54.0",
|
|
46
|
+
"vitest": "4.0.18"
|
|
47
|
+
}
|
|
48
|
+
}
|