@juppytt/fws 0.1.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/.claude/settings.local.json +72 -0
- package/README.md +126 -0
- package/bin/fws-cli.sh +4 -0
- package/bin/fws.ts +421 -0
- package/docs/cli-reference.md +211 -0
- package/docs/gws-support.md +276 -0
- package/package.json +28 -0
- package/src/config/rewrite-cache.ts +73 -0
- package/src/index.ts +3 -0
- package/src/proxy/mitm.ts +285 -0
- package/src/server/app.ts +26 -0
- package/src/server/middleware.ts +38 -0
- package/src/server/routes/calendar.ts +483 -0
- package/src/server/routes/control.ts +151 -0
- package/src/server/routes/drive.ts +342 -0
- package/src/server/routes/gmail.ts +758 -0
- package/src/server/routes/people.ts +239 -0
- package/src/server/routes/sheets.ts +242 -0
- package/src/server/routes/tasks.ts +191 -0
- package/src/store/index.ts +24 -0
- package/src/store/seed.ts +313 -0
- package/src/store/types.ts +225 -0
- package/src/util/id.ts +9 -0
- package/test/calendar.test.ts +227 -0
- package/test/drive.test.ts +153 -0
- package/test/gmail.test.ts +215 -0
- package/test/gws-validation.test.ts +883 -0
- package/test/helpers/harness.ts +109 -0
- package/test/snapshot.test.ts +80 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createApp } from '../../src/server/app.js';
|
|
2
|
+
import { resetStore } from '../../src/store/index.js';
|
|
3
|
+
import { generateConfigDir } from '../../src/config/rewrite-cache.js';
|
|
4
|
+
import { generateCACert, startMitmProxy } from '../../src/proxy/mitm.js';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import type { Server } from 'node:http';
|
|
10
|
+
|
|
11
|
+
export interface TestHarness {
|
|
12
|
+
port: number;
|
|
13
|
+
/** Direct HTTP fetch against mock server */
|
|
14
|
+
fetch: (urlPath: string, init?: RequestInit) => Promise<Response>;
|
|
15
|
+
/** Run gws command (uses discovery cache rewriting for regular commands) */
|
|
16
|
+
gws: (args: string) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
|
|
17
|
+
/** Run gws command with MITM proxy (for helper commands like +triage) */
|
|
18
|
+
gwsProxy: (args: string) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
|
|
19
|
+
cleanup: () => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function createTestHarness(): Promise<TestHarness> {
|
|
23
|
+
resetStore();
|
|
24
|
+
|
|
25
|
+
const app = createApp();
|
|
26
|
+
const server: Server = await new Promise((resolve) => {
|
|
27
|
+
const s = app.listen(0, () => resolve(s));
|
|
28
|
+
});
|
|
29
|
+
const port = (server.address() as any).port as number;
|
|
30
|
+
|
|
31
|
+
const configDir = await mkdtemp(path.join(tmpdir(), 'fws-test-'));
|
|
32
|
+
await generateConfigDir(port, configDir);
|
|
33
|
+
|
|
34
|
+
// MITM proxy for helper commands
|
|
35
|
+
const dataDir = await mkdtemp(path.join(tmpdir(), 'fws-test-data-'));
|
|
36
|
+
const { caPath } = await generateCACert(dataDir);
|
|
37
|
+
const proxyServer = startMitmProxy(port, 0); // random port
|
|
38
|
+
const proxyPort = (proxyServer.address() as any).port as number;
|
|
39
|
+
|
|
40
|
+
const baseEnv = {
|
|
41
|
+
...process.env,
|
|
42
|
+
GOOGLE_WORKSPACE_CLI_CONFIG_DIR: configDir,
|
|
43
|
+
GOOGLE_WORKSPACE_CLI_TOKEN: 'fake',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const proxyEnv = {
|
|
47
|
+
...baseEnv,
|
|
48
|
+
HTTPS_PROXY: `http://localhost:${proxyPort}`,
|
|
49
|
+
SSL_CERT_FILE: caPath,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const gwsPath = process.env.GWS_PATH || 'gws';
|
|
53
|
+
|
|
54
|
+
function parseArgs(args: string): string[] {
|
|
55
|
+
// Split on whitespace but keep JSON objects and quoted strings as single args
|
|
56
|
+
const result: string[] = [];
|
|
57
|
+
let current = '';
|
|
58
|
+
let braceDepth = 0;
|
|
59
|
+
let inQuote: string | null = null;
|
|
60
|
+
for (const char of args) {
|
|
61
|
+
if (!inQuote && !braceDepth && (char === '"' || char === "'")) {
|
|
62
|
+
inQuote = char;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (inQuote && char === inQuote) {
|
|
66
|
+
inQuote = null;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!inQuote) {
|
|
70
|
+
if (char === '{') braceDepth++;
|
|
71
|
+
if (char === '}') braceDepth--;
|
|
72
|
+
}
|
|
73
|
+
if (char === ' ' && braceDepth === 0 && !inQuote) {
|
|
74
|
+
if (current) result.push(current);
|
|
75
|
+
current = '';
|
|
76
|
+
} else {
|
|
77
|
+
current += char;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (current) result.push(current);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function runGws(args: string, env: Record<string, string | undefined>): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
execFile(gwsPath, parseArgs(args), { env, timeout: 10000 }, (err, stdout, stderr) => {
|
|
87
|
+
resolve({
|
|
88
|
+
stdout: stdout || '',
|
|
89
|
+
stderr: stderr || '',
|
|
90
|
+
exitCode: err ? (err as any).code ?? 1 : 0,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
port,
|
|
98
|
+
fetch: (urlPath: string, init?: RequestInit) =>
|
|
99
|
+
globalThis.fetch(`http://localhost:${port}${urlPath}`, init),
|
|
100
|
+
gws: (args: string) => runGws(args, baseEnv),
|
|
101
|
+
gwsProxy: (args: string) => runGws(args, proxyEnv),
|
|
102
|
+
cleanup: async () => {
|
|
103
|
+
server.close();
|
|
104
|
+
proxyServer.close();
|
|
105
|
+
await rm(configDir, { recursive: true, force: true });
|
|
106
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { createTestHarness, type TestHarness } from './helpers/harness.js';
|
|
3
|
+
|
|
4
|
+
describe('Snapshot', () => {
|
|
5
|
+
let h: TestHarness;
|
|
6
|
+
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
h = await createTestHarness();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterAll(async () => {
|
|
12
|
+
await h.cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('save returns current state as JSON', async () => {
|
|
16
|
+
const res = await h.fetch('/__fws/snapshot/save', { method: 'POST' });
|
|
17
|
+
const data = await res.json();
|
|
18
|
+
expect(data.gmail).toBeDefined();
|
|
19
|
+
expect(data.calendar).toBeDefined();
|
|
20
|
+
expect(data.drive).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('full roundtrip: add data, snapshot, reset, verify empty, load, verify data', async () => {
|
|
24
|
+
// 1. Add some data
|
|
25
|
+
await h.fetch('/__fws/setup/gmail/message', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({ from: 'snap@example.com', subject: 'Snapshot Test', body: 'snapshot body' }),
|
|
29
|
+
});
|
|
30
|
+
await h.fetch('/__fws/setup/calendar/event', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ summary: 'Snapshot Event', start: '2026-06-01T10:00:00Z', duration: '1h' }),
|
|
34
|
+
});
|
|
35
|
+
await h.fetch('/__fws/setup/drive/file', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({ name: 'snapshot-file.txt' }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// 2. Snapshot
|
|
42
|
+
const snapRes = await h.fetch('/__fws/snapshot/save', { method: 'POST' });
|
|
43
|
+
const snapshot = await snapRes.json();
|
|
44
|
+
expect(Object.keys(snapshot.gmail.messages).length).toBeGreaterThan(0);
|
|
45
|
+
|
|
46
|
+
// 3. Reset
|
|
47
|
+
await h.fetch('/__fws/reset', { method: 'POST' });
|
|
48
|
+
|
|
49
|
+
// 4. Verify reset to seed (5 seed messages, 5 seed files — the extras we added are gone)
|
|
50
|
+
const msgsRes = await h.fetch('/gmail/v1/users/me/messages');
|
|
51
|
+
const msgs = await msgsRes.json();
|
|
52
|
+
expect(msgs.messages.length).toBe(5); // only seed messages
|
|
53
|
+
|
|
54
|
+
const filesRes = await h.fetch('/drive/v3/files');
|
|
55
|
+
const files = await filesRes.json();
|
|
56
|
+
expect(files.files.length).toBe(5); // only seed files
|
|
57
|
+
|
|
58
|
+
// 5. Load snapshot
|
|
59
|
+
await h.fetch('/__fws/snapshot/load', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify(snapshot),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 6. Verify data restored (seed + our extras)
|
|
66
|
+
const msgsRes2 = await h.fetch('/gmail/v1/users/me/messages');
|
|
67
|
+
const msgs2 = await msgsRes2.json();
|
|
68
|
+
expect(msgs2.messages.length).toBeGreaterThan(5); // seed + added
|
|
69
|
+
|
|
70
|
+
const filesRes2 = await h.fetch('/drive/v3/files');
|
|
71
|
+
const files2 = await filesRes2.json();
|
|
72
|
+
expect(files2.files.length).toBeGreaterThan(5); // seed + added
|
|
73
|
+
|
|
74
|
+
// Verify via gws
|
|
75
|
+
const { stdout, exitCode } = await h.gws('gmail users messages list --params {"userId":"me"}');
|
|
76
|
+
expect(exitCode).toBe(0);
|
|
77
|
+
const gwsMsgs = JSON.parse(stdout);
|
|
78
|
+
expect(gwsMsgs.messages.length).toBeGreaterThan(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*", "bin/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
17
|
+
}
|