google-drive-mock 1.0.9 → 1.0.10
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/mappers.js +2 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.js +60 -2
- package/package.json +2 -2
- package/scripts/check-token.ts +75 -0
- package/src/mappers.ts +2 -0
- package/src/store.ts +34 -0
- package/test/mime_types.test.ts +1 -1
- package/test/missing_fields.test.ts +123 -0
package/dist/mappers.js
CHANGED
package/dist/store.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface DriveFile {
|
|
|
9
9
|
trashed: boolean;
|
|
10
10
|
createdTime: string;
|
|
11
11
|
modifiedTime: string;
|
|
12
|
+
size: string;
|
|
13
|
+
md5Checksum: string;
|
|
12
14
|
[key: string]: unknown;
|
|
13
15
|
}
|
|
14
16
|
export interface DriveChange {
|
|
@@ -38,6 +40,7 @@ export declare class DriveStore {
|
|
|
38
40
|
private files;
|
|
39
41
|
private changes;
|
|
40
42
|
constructor();
|
|
43
|
+
private calculateStats;
|
|
41
44
|
createFile(file: Partial<DriveFile> & {
|
|
42
45
|
name: string;
|
|
43
46
|
}): DriveFile;
|
package/dist/store.js
CHANGED
|
@@ -1,18 +1,71 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.driveStore = exports.DriveStore = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
4
38
|
class DriveStore {
|
|
5
39
|
constructor() {
|
|
6
40
|
this.files = new Map();
|
|
7
41
|
this.changes = [];
|
|
8
42
|
}
|
|
43
|
+
calculateStats(content) {
|
|
44
|
+
let buffer;
|
|
45
|
+
if (typeof content === 'string') {
|
|
46
|
+
buffer = Buffer.from(content);
|
|
47
|
+
}
|
|
48
|
+
else if (content === undefined || content === null) {
|
|
49
|
+
buffer = Buffer.from('');
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
buffer = Buffer.from(JSON.stringify(content));
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
size: String(buffer.length),
|
|
56
|
+
md5Checksum: crypto.createHash('md5').update(buffer).digest('hex')
|
|
57
|
+
};
|
|
58
|
+
}
|
|
9
59
|
createFile(file) {
|
|
10
60
|
if (!file.name) {
|
|
11
61
|
throw new Error("File name is required");
|
|
12
62
|
}
|
|
13
63
|
const id = file.id || Math.random().toString(36).substring(7);
|
|
14
64
|
const now = new Date().toISOString();
|
|
15
|
-
const
|
|
65
|
+
const stats = this.calculateStats(file.content);
|
|
66
|
+
const newFile = Object.assign(Object.assign({ kind: "drive#file", mimeType: "application/octet-stream", trashed: false, createdTime: now, modifiedTime: now }, file), { id, version: 1, etag: "1",
|
|
67
|
+
// Ensure calculated stats override provided ones
|
|
68
|
+
size: stats.size, md5Checksum: stats.md5Checksum });
|
|
16
69
|
this.files.set(id, newFile);
|
|
17
70
|
this.addChange(newFile);
|
|
18
71
|
return newFile;
|
|
@@ -21,9 +74,14 @@ class DriveStore {
|
|
|
21
74
|
const file = this.files.get(id);
|
|
22
75
|
if (!file)
|
|
23
76
|
return null;
|
|
77
|
+
// If content is being updated, recalculate stats
|
|
78
|
+
let statsUpdates = {};
|
|
79
|
+
if (updates.content !== undefined) {
|
|
80
|
+
statsUpdates = this.calculateStats(updates.content);
|
|
81
|
+
}
|
|
24
82
|
// Merge updates and increment version
|
|
25
83
|
const newVersion = file.version + 1;
|
|
26
|
-
const updatedFile = Object.assign(Object.assign(Object.assign({}, file), updates), { version: newVersion, etag: String(newVersion), modifiedTime: new Date().toISOString() });
|
|
84
|
+
const updatedFile = Object.assign(Object.assign(Object.assign(Object.assign({}, file), updates), statsUpdates), { version: newVersion, etag: String(newVersion), modifiedTime: new Date().toISOString() });
|
|
27
85
|
this.files.set(id, updatedFile);
|
|
28
86
|
this.addChange(updatedFile);
|
|
29
87
|
return updatedFile;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-drive-mock",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Mock-Server that simulates being google-drive. Used for testing.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test:slow": "LATENCY=20 vitest run",
|
|
13
13
|
"test:browser": "start-server-and-test dev http://localhost:3000 'TEST_TARGET=mock BROWSER_ENABLED=true vitest run --browser --no-file-parallelism'",
|
|
14
14
|
"test:browser:real": "TEST_TARGET=real BROWSER_ENABLED=true vitest run --browser",
|
|
15
|
-
"test:real": "TEST_TARGET=real vitest run",
|
|
15
|
+
"test:real": "ts-node scripts/check-token.ts && TEST_TARGET=real vitest run",
|
|
16
16
|
"example:login": "ts-node examples/serve-login.ts",
|
|
17
17
|
"lint": "eslint .",
|
|
18
18
|
"lint:fix": "eslint . --fix"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as https from 'https';
|
|
4
|
+
|
|
5
|
+
// Load .ENV manually to avoid devDependency issues if dotenv isn't available in this context,
|
|
6
|
+
// though project seems to use it.
|
|
7
|
+
const envPath = path.resolve(__dirname, '../.ENV');
|
|
8
|
+
if (fs.existsSync(envPath)) {
|
|
9
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
10
|
+
envContent.split('\n').forEach(line => {
|
|
11
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
12
|
+
if (match) {
|
|
13
|
+
const key = match[1].trim();
|
|
14
|
+
const value = match[2].trim().replace(/^['"]|['"]$/g, ''); // strip quotes
|
|
15
|
+
if (!process.env[key]) {
|
|
16
|
+
process.env[key] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const token = process.env.GDRIVE_TOKEN;
|
|
23
|
+
|
|
24
|
+
if (!token) {
|
|
25
|
+
console.error('❌ Error: GDRIVE_TOKEN not found in environment or .ENV file.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Simple check only if running real tests (though script is likely invoked specifically for that)
|
|
30
|
+
// The user asked to run this BEFORE test:real.
|
|
31
|
+
|
|
32
|
+
console.log('🔄 Verifying GDRIVE_TOKEN...');
|
|
33
|
+
|
|
34
|
+
const options = {
|
|
35
|
+
hostname: 'www.googleapis.com',
|
|
36
|
+
path: '/drive/v3/about?fields=user',
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: {
|
|
39
|
+
'Authorization': `Bearer ${token}`,
|
|
40
|
+
'User-Agent': 'node-script'
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const req = https.request(options, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
|
|
47
|
+
res.on('data', (chunk) => {
|
|
48
|
+
data += chunk;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
res.on('end', () => {
|
|
52
|
+
if (res.statusCode === 200) {
|
|
53
|
+
try {
|
|
54
|
+
const body = JSON.parse(data);
|
|
55
|
+
console.log(`✅ Token is valid. User: ${body.user?.emailAddress || 'Unknown'}`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
} catch (e: unknown) {
|
|
58
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
59
|
+
console.error('❌ Error parsing response:', msg);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.error(`❌ Token verification failed. Tell the human to update the .ENV file with a valid token. Status: ${res.statusCode}`);
|
|
64
|
+
console.error('Response:', data);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
req.on('error', (e) => {
|
|
71
|
+
console.error(`❌ Request error: ${e.message}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
req.end();
|
package/src/mappers.ts
CHANGED
|
@@ -28,6 +28,8 @@ export function toV2File(file: DriveFile): Record<string, unknown> {
|
|
|
28
28
|
isRoot: false // Mock simplification
|
|
29
29
|
})),
|
|
30
30
|
version: file.version,
|
|
31
|
+
fileSize: file.size,
|
|
32
|
+
md5Checksum: file.md5Checksum,
|
|
31
33
|
downloadUrl: `http://localhost/drive/v2/files/${file.id}?alt=media`
|
|
32
34
|
};
|
|
33
35
|
}
|
package/src/store.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
|
|
1
3
|
export interface DriveFile {
|
|
2
4
|
id: string;
|
|
3
5
|
name: string;
|
|
@@ -9,6 +11,8 @@ export interface DriveFile {
|
|
|
9
11
|
trashed: boolean;
|
|
10
12
|
createdTime: string;
|
|
11
13
|
modifiedTime: string;
|
|
14
|
+
size: string;
|
|
15
|
+
md5Checksum: string;
|
|
12
16
|
[key: string]: unknown;
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -46,22 +50,45 @@ export class DriveStore {
|
|
|
46
50
|
this.changes = [];
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
private calculateStats(content: unknown): { size: string, md5Checksum: string } {
|
|
54
|
+
let buffer: Buffer;
|
|
55
|
+
if (typeof content === 'string') {
|
|
56
|
+
buffer = Buffer.from(content);
|
|
57
|
+
} else if (content === undefined || content === null) {
|
|
58
|
+
buffer = Buffer.from('');
|
|
59
|
+
} else {
|
|
60
|
+
buffer = Buffer.from(JSON.stringify(content));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
size: String(buffer.length),
|
|
65
|
+
md5Checksum: crypto.createHash('md5').update(buffer).digest('hex')
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
createFile(file: Partial<DriveFile> & { name: string }): DriveFile {
|
|
50
70
|
if (!file.name) {
|
|
51
71
|
throw new Error("File name is required");
|
|
52
72
|
}
|
|
53
73
|
const id = file.id || Math.random().toString(36).substring(7);
|
|
54
74
|
const now = new Date().toISOString();
|
|
75
|
+
|
|
76
|
+
const stats = this.calculateStats(file.content);
|
|
77
|
+
|
|
55
78
|
const newFile: DriveFile = {
|
|
56
79
|
kind: "drive#file",
|
|
57
80
|
mimeType: "application/octet-stream",
|
|
58
81
|
trashed: false,
|
|
59
82
|
createdTime: now,
|
|
60
83
|
modifiedTime: now,
|
|
84
|
+
|
|
61
85
|
...file,
|
|
62
86
|
id,
|
|
63
87
|
version: 1, // Initialize version
|
|
64
88
|
etag: "1", // Initialize etag
|
|
89
|
+
// Ensure calculated stats override provided ones
|
|
90
|
+
size: stats.size,
|
|
91
|
+
md5Checksum: stats.md5Checksum
|
|
65
92
|
};
|
|
66
93
|
|
|
67
94
|
this.files.set(id, newFile);
|
|
@@ -73,11 +100,18 @@ export class DriveStore {
|
|
|
73
100
|
const file = this.files.get(id);
|
|
74
101
|
if (!file) return null;
|
|
75
102
|
|
|
103
|
+
// If content is being updated, recalculate stats
|
|
104
|
+
let statsUpdates = {};
|
|
105
|
+
if (updates.content !== undefined) {
|
|
106
|
+
statsUpdates = this.calculateStats(updates.content);
|
|
107
|
+
}
|
|
108
|
+
|
|
76
109
|
// Merge updates and increment version
|
|
77
110
|
const newVersion = file.version + 1;
|
|
78
111
|
const updatedFile = {
|
|
79
112
|
...file,
|
|
80
113
|
...updates,
|
|
114
|
+
...statsUpdates,
|
|
81
115
|
version: newVersion,
|
|
82
116
|
etag: String(newVersion),
|
|
83
117
|
modifiedTime: new Date().toISOString()
|
package/test/mime_types.test.ts
CHANGED
|
@@ -54,7 +54,7 @@ describe('MIME Type Handling', () => {
|
|
|
54
54
|
expect(await getRes.json()).toEqual(content);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it('should update MIME type and return new Content-Type header', async () => {
|
|
57
|
+
it('should update MIME type and return new Content-Type header', { timeout: 10000 }, async () => {
|
|
58
58
|
// Create as text/plain
|
|
59
59
|
const createRes = await fetch(`${config.baseUrl}/drive/v3/files`, {
|
|
60
60
|
method: 'POST',
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { getTestConfig, TestConfig } from './config';
|
|
3
|
+
|
|
4
|
+
describe('Missing Fields Support (Size & MD5)', () => {
|
|
5
|
+
let config: TestConfig;
|
|
6
|
+
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
config = await getTestConfig();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterAll(() => {
|
|
12
|
+
if (config) config.stop();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return size and md5Checksum in V3 GET', async () => {
|
|
16
|
+
const content = JSON.stringify({ foo: 'bar' });
|
|
17
|
+
const metadata = { name: 'V3 Size Test', mimeType: 'application/json' };
|
|
18
|
+
|
|
19
|
+
const boundary = 'foo_bar_baz';
|
|
20
|
+
const delimiter = `\r\n--${boundary}\r\n`;
|
|
21
|
+
const closeDelim = `\r\n--${boundary}--`;
|
|
22
|
+
|
|
23
|
+
const body = delimiter +
|
|
24
|
+
'Content-Type: application/json\r\n\r\n' +
|
|
25
|
+
JSON.stringify(metadata) +
|
|
26
|
+
delimiter +
|
|
27
|
+
'Content-Type: application/json\r\n\r\n' +
|
|
28
|
+
content +
|
|
29
|
+
closeDelim;
|
|
30
|
+
|
|
31
|
+
const createRes = await fetch(`${config.baseUrl}/upload/drive/v3/files?uploadType=multipart`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Authorization': `Bearer ${config.token}`,
|
|
35
|
+
'Content-Type': `multipart/related; boundary="${boundary}"`
|
|
36
|
+
},
|
|
37
|
+
body: body
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(createRes.status).toBe(200);
|
|
41
|
+
const file = await createRes.json();
|
|
42
|
+
|
|
43
|
+
const fields = 'id,name,size,md5Checksum';
|
|
44
|
+
const getRes = await fetch(`${config.baseUrl}/drive/v3/files/${file.id}?fields=${fields}`, {
|
|
45
|
+
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
46
|
+
});
|
|
47
|
+
expect(getRes.status).toBe(200);
|
|
48
|
+
const getFile = await getRes.json();
|
|
49
|
+
|
|
50
|
+
expect(getFile.size).toBeDefined();
|
|
51
|
+
// size should be string in V3
|
|
52
|
+
expect(String(getFile.size)).toBe(String(content.length));
|
|
53
|
+
expect(getFile.md5Checksum).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return fileSize and md5Checksum in V2 GET', async () => {
|
|
57
|
+
const content = 'V2 Content';
|
|
58
|
+
|
|
59
|
+
// Use simple upload for V2 to ensure content and metadata
|
|
60
|
+
const uploadRes = await fetch(`${config.baseUrl}/upload/drive/v2/files?uploadType=media`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'Authorization': `Bearer ${config.token}`,
|
|
64
|
+
'Content-Type': 'text/plain'
|
|
65
|
+
},
|
|
66
|
+
body: content
|
|
67
|
+
});
|
|
68
|
+
expect(uploadRes.status).toBe(200);
|
|
69
|
+
const file = await uploadRes.json();
|
|
70
|
+
|
|
71
|
+
expect(file.fileSize).toBeDefined(); // V2 uses fileSize
|
|
72
|
+
expect(Number(file.fileSize)).toBe(content.length);
|
|
73
|
+
expect(file.md5Checksum).toBeDefined();
|
|
74
|
+
|
|
75
|
+
// Verify GET
|
|
76
|
+
const getRes = await fetch(`${config.baseUrl}/drive/v2/files/${file.id}`, {
|
|
77
|
+
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
78
|
+
});
|
|
79
|
+
const getFile = await getRes.json();
|
|
80
|
+
expect(getFile.fileSize).toBeDefined();
|
|
81
|
+
expect(Number(getFile.fileSize)).toBe(content.length);
|
|
82
|
+
expect(getFile.md5Checksum).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should return correct size and md5Checksum for EMPTY file', async () => {
|
|
86
|
+
const content = '';
|
|
87
|
+
const md5Empty = 'd41d8cd98f00b204e9800998ecf8427e'; // MD5 of empty string
|
|
88
|
+
|
|
89
|
+
// Upload empty file via V2 media upload
|
|
90
|
+
const uploadRes = await fetch(`${config.baseUrl}/upload/drive/v2/files?uploadType=media`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Authorization': `Bearer ${config.token}`,
|
|
94
|
+
'Content-Type': 'text/plain'
|
|
95
|
+
},
|
|
96
|
+
body: content
|
|
97
|
+
});
|
|
98
|
+
expect(uploadRes.status).toBe(200);
|
|
99
|
+
const file = await uploadRes.json();
|
|
100
|
+
|
|
101
|
+
// Check response
|
|
102
|
+
expect(file.fileSize).toBeDefined();
|
|
103
|
+
expect(Number(file.fileSize)).toBe(0);
|
|
104
|
+
expect(file.md5Checksum).toBe(md5Empty);
|
|
105
|
+
|
|
106
|
+
// Verify GET V2
|
|
107
|
+
const getV2 = await fetch(`${config.baseUrl}/drive/v2/files/${file.id}`, {
|
|
108
|
+
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
109
|
+
});
|
|
110
|
+
const v2File = await getV2.json();
|
|
111
|
+
expect(Number(v2File.fileSize)).toBe(0);
|
|
112
|
+
expect(v2File.md5Checksum).toBe(md5Empty);
|
|
113
|
+
|
|
114
|
+
// Verify GET V3
|
|
115
|
+
const fields = 'id,name,size,md5Checksum';
|
|
116
|
+
const getV3 = await fetch(`${config.baseUrl}/drive/v3/files/${file.id}?fields=${fields}`, {
|
|
117
|
+
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
118
|
+
});
|
|
119
|
+
const v3File = await getV3.json();
|
|
120
|
+
expect(Number(v3File.size)).toBe(0);
|
|
121
|
+
expect(v3File.md5Checksum).toBe(md5Empty);
|
|
122
|
+
});
|
|
123
|
+
});
|