@mjasano/devtunnel 1.1.0 → 1.4.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 +5 -1
- package/.prettierignore +2 -0
- package/.prettierrc +8 -0
- package/CHANGELOG.md +78 -0
- package/CLAUDE.md +18 -0
- package/README.md +138 -0
- package/bin/cli.js +40 -11
- package/eslint.config.js +32 -0
- package/package.json +12 -3
- package/public/app.js +839 -0
- package/public/index.html +13 -1247
- package/public/login.html +242 -0
- package/public/styles.css +857 -0
- package/server.js +585 -8
- package/test/server.test.js +204 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const { describe, it, before, after } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const { spawn } = require('node:child_process');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
let serverProcess;
|
|
8
|
+
const serverPort = 3099;
|
|
9
|
+
|
|
10
|
+
function makeRequest(method, path, body = null) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const options = {
|
|
13
|
+
hostname: 'localhost',
|
|
14
|
+
port: serverPort,
|
|
15
|
+
path,
|
|
16
|
+
method,
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const req = http.request(options, (res) => {
|
|
23
|
+
let data = '';
|
|
24
|
+
res.on('data', (chunk) => (data += chunk));
|
|
25
|
+
res.on('end', () => {
|
|
26
|
+
try {
|
|
27
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
28
|
+
} catch {
|
|
29
|
+
resolve({ status: res.statusCode, data });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
req.on('error', reject);
|
|
35
|
+
|
|
36
|
+
if (body) {
|
|
37
|
+
req.write(JSON.stringify(body));
|
|
38
|
+
}
|
|
39
|
+
req.end();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function waitForServer(port, timeout = 10000) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const start = Date.now();
|
|
46
|
+
|
|
47
|
+
const check = () => {
|
|
48
|
+
const req = http.get(`http://localhost:${port}/health`, (res) => {
|
|
49
|
+
if (res.statusCode === 200) {
|
|
50
|
+
resolve();
|
|
51
|
+
} else {
|
|
52
|
+
retry();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
req.on('error', retry);
|
|
57
|
+
req.setTimeout(1000);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const retry = () => {
|
|
61
|
+
if (Date.now() - start > timeout) {
|
|
62
|
+
reject(new Error('Server startup timeout'));
|
|
63
|
+
} else {
|
|
64
|
+
setTimeout(check, 200);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
check();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('DevTunnel Server API', () => {
|
|
73
|
+
before(async () => {
|
|
74
|
+
const serverPath = path.join(__dirname, '..', 'server.js');
|
|
75
|
+
serverProcess = spawn('node', [serverPath], {
|
|
76
|
+
env: { ...process.env, PORT: serverPort },
|
|
77
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
serverProcess.stdout.on('data', (data) => {
|
|
81
|
+
console.log(`[server] ${data.toString().trim()}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
serverProcess.stderr.on('data', (data) => {
|
|
85
|
+
console.error(`[server] ${data.toString().trim()}`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await waitForServer(serverPort);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
after(() => {
|
|
92
|
+
if (serverProcess) {
|
|
93
|
+
serverProcess.kill('SIGTERM');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('GET /health', () => {
|
|
98
|
+
it('should return health status', async () => {
|
|
99
|
+
const { status, data } = await makeRequest('GET', '/health');
|
|
100
|
+
assert.strictEqual(status, 200);
|
|
101
|
+
assert.strictEqual(data.status, 'ok');
|
|
102
|
+
assert.strictEqual(typeof data.sessions, 'number');
|
|
103
|
+
assert.strictEqual(typeof data.tunnels, 'number');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('GET /api/system', () => {
|
|
108
|
+
it('should return system information', async () => {
|
|
109
|
+
const { status, data } = await makeRequest('GET', '/api/system');
|
|
110
|
+
assert.strictEqual(status, 200);
|
|
111
|
+
assert.ok(data.hostname);
|
|
112
|
+
assert.ok(data.platform);
|
|
113
|
+
assert.ok(data.arch);
|
|
114
|
+
assert.strictEqual(typeof data.uptime, 'number');
|
|
115
|
+
assert.ok(data.cpu);
|
|
116
|
+
assert.ok(data.memory);
|
|
117
|
+
assert.ok(Array.isArray(data.loadavg));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('GET /api/sessions', () => {
|
|
122
|
+
it('should return empty session list initially', async () => {
|
|
123
|
+
const { status, data } = await makeRequest('GET', '/api/sessions');
|
|
124
|
+
assert.strictEqual(status, 200);
|
|
125
|
+
assert.ok(Array.isArray(data));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('GET /api/tunnels', () => {
|
|
130
|
+
it('should return empty tunnel list initially', async () => {
|
|
131
|
+
const { status, data } = await makeRequest('GET', '/api/tunnels');
|
|
132
|
+
assert.strictEqual(status, 200);
|
|
133
|
+
assert.ok(Array.isArray(data));
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('GET /api/files', () => {
|
|
138
|
+
it('should return file listing', async () => {
|
|
139
|
+
const { status, data } = await makeRequest('GET', '/api/files');
|
|
140
|
+
assert.strictEqual(status, 200);
|
|
141
|
+
assert.strictEqual(data.path, '');
|
|
142
|
+
assert.ok(Array.isArray(data.items));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('POST /api/files/write', () => {
|
|
147
|
+
it('should reject request without path', async () => {
|
|
148
|
+
const { status, data } = await makeRequest('POST', '/api/files/write', {
|
|
149
|
+
content: 'test',
|
|
150
|
+
});
|
|
151
|
+
assert.strictEqual(status, 400);
|
|
152
|
+
assert.strictEqual(data.error, 'Path required');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('POST /api/files/delete', () => {
|
|
157
|
+
it('should reject request without path', async () => {
|
|
158
|
+
const { status, data } = await makeRequest('POST', '/api/files/delete', {});
|
|
159
|
+
assert.strictEqual(status, 400);
|
|
160
|
+
assert.strictEqual(data.error, 'Path required');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should return 404 for non-existent file', async () => {
|
|
164
|
+
const { status, data } = await makeRequest('POST', '/api/files/delete', {
|
|
165
|
+
path: 'non-existent-file-12345.txt',
|
|
166
|
+
});
|
|
167
|
+
assert.strictEqual(status, 404);
|
|
168
|
+
assert.strictEqual(data.error, 'File not found');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('POST /api/files/rename', () => {
|
|
173
|
+
it('should reject request without paths', async () => {
|
|
174
|
+
const { status, data } = await makeRequest('POST', '/api/files/rename', {});
|
|
175
|
+
assert.strictEqual(status, 400);
|
|
176
|
+
assert.strictEqual(data.error, 'Both oldPath and newPath required');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return 404 for non-existent source file', async () => {
|
|
180
|
+
const { status, data } = await makeRequest('POST', '/api/files/rename', {
|
|
181
|
+
oldPath: 'non-existent-file-12345.txt',
|
|
182
|
+
newPath: 'new-name.txt',
|
|
183
|
+
});
|
|
184
|
+
assert.strictEqual(status, 404);
|
|
185
|
+
assert.strictEqual(data.error, 'File not found');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('POST /api/tunnels', () => {
|
|
190
|
+
it('should reject request without valid port', async () => {
|
|
191
|
+
const { status, data } = await makeRequest('POST', '/api/tunnels', {});
|
|
192
|
+
assert.strictEqual(status, 400);
|
|
193
|
+
assert.strictEqual(data.error, 'Valid port number required');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should reject request with invalid port type', async () => {
|
|
197
|
+
const { status, data } = await makeRequest('POST', '/api/tunnels', {
|
|
198
|
+
port: 'invalid',
|
|
199
|
+
});
|
|
200
|
+
assert.strictEqual(status, 400);
|
|
201
|
+
assert.strictEqual(data.error, 'Valid port number required');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|