Skip to content

Commit fd16520

Browse files
Add commons.test.js
1 parent 6b47c15 commit fd16520

File tree

1 file changed

+394
-0
lines changed

1 file changed

+394
-0
lines changed

tests/commons.test.js

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
3+
vi.spyOn(console, 'log').mockImplementation(() => { });
4+
vi.spyOn(console, 'error').mockImplementation(() => { });
5+
6+
const mockGetAuthToken = vi.hoisted(() => vi.fn(() => 'mock-token'));
7+
const mockFormatSize = vi.hoisted(() => vi.fn((size) => `${size} bytes`));
8+
const mockReadFile = vi.hoisted(() => vi.fn());
9+
10+
vi.mock('../src/commands/auth.js', () => ({
11+
getAuthToken: mockGetAuthToken,
12+
}));
13+
14+
vi.mock('../src/utils.js', () => ({
15+
formatSize: mockFormatSize,
16+
}));
17+
18+
vi.mock('fs/promises', () => ({
19+
readFile: mockReadFile,
20+
}));
21+
22+
vi.mock('dotenv', () => ({
23+
default: {
24+
config: vi.fn(),
25+
},
26+
}));
27+
28+
let commons;
29+
30+
beforeEach(async () => {
31+
vi.resetModules();
32+
mockGetAuthToken.mockReturnValue('mock-token');
33+
commons = await import('../src/commons.js');
34+
});
35+
36+
afterEach(() => {
37+
vi.clearAllMocks();
38+
});
39+
40+
describe('constants', () => {
41+
it('should export PROJECT_NAME', () => {
42+
expect(commons.PROJECT_NAME).toBe('puter-cli');
43+
});
44+
45+
it('should export NULL_UUID', () => {
46+
expect(commons.NULL_UUID).toBe('00000000-0000-0000-0000-000000000000');
47+
});
48+
49+
it('should have default API_BASE', () => {
50+
expect(commons.API_BASE).toBeDefined();
51+
});
52+
53+
it('should have default BASE_URL', () => {
54+
expect(commons.BASE_URL).toBeDefined();
55+
});
56+
});
57+
58+
describe('reconfigureURLs', () => {
59+
it('should update API_BASE and BASE_URL', async () => {
60+
vi.resetModules();
61+
const freshCommons = await import('../src/commons.js');
62+
63+
freshCommons.reconfigureURLs({ api: 'https://new-api.example.com', base: 'https://new.example.com' });
64+
65+
expect(freshCommons.API_BASE).toBe('https://new-api.example.com');
66+
expect(freshCommons.BASE_URL).toBe('https://new.example.com');
67+
});
68+
});
69+
70+
describe('getHeaders', () => {
71+
it('should return headers with default content type', () => {
72+
const headers = commons.getHeaders();
73+
74+
expect(headers['Content-Type']).toBe('application/json');
75+
expect(headers['Authorization']).toBe('Bearer mock-token');
76+
expect(headers['Accept']).toBe('*/*');
77+
expect(headers['Accept-Language']).toBe('en-US,en;q=0.9');
78+
expect(headers['Connection']).toBe('keep-alive');
79+
});
80+
81+
it('should return headers with custom content type', () => {
82+
const headers = commons.getHeaders('multipart/form-data');
83+
84+
expect(headers['Content-Type']).toBe('multipart/form-data');
85+
});
86+
87+
it('should include Origin and Referer based on BASE_URL', async () => {
88+
vi.resetModules();
89+
const freshCommons = await import('../src/commons.js');
90+
freshCommons.reconfigureURLs({ api: 'https://api.test.com', base: 'https://test.com' });
91+
92+
const headers = freshCommons.getHeaders();
93+
94+
expect(headers['Origin']).toBe('https://test.com');
95+
expect(headers['Referer']).toBe('https://test.com/');
96+
});
97+
});
98+
99+
describe('generateAppName', () => {
100+
it('should generate a name with default separator', () => {
101+
const name = commons.generateAppName();
102+
103+
expect(name).toMatch(/^[a-z]+-[a-z]+-\d+$/);
104+
});
105+
106+
it('should generate a name with custom separator', () => {
107+
const name = commons.generateAppName('_');
108+
109+
expect(name).toMatch(/^[a-z]+_[a-z]+_\d+$/);
110+
});
111+
});
112+
113+
describe('displayTable', () => {
114+
it('should display a table with headers and data', () => {
115+
const consoleLogSpy = vi.spyOn(console, 'log');
116+
117+
const data = [
118+
{ name: 'App1', status: 'running' },
119+
{ name: 'App2', status: 'stopped' },
120+
];
121+
122+
commons.displayTable(data, {
123+
headers: ['Name', 'Status'],
124+
columns: ['name', 'status'],
125+
columnWidth: 15,
126+
});
127+
128+
expect(consoleLogSpy).toHaveBeenCalled();
129+
});
130+
131+
it('should handle empty data', () => {
132+
const consoleLogSpy = vi.spyOn(console, 'log');
133+
134+
commons.displayTable([], {
135+
headers: ['Name'],
136+
columns: ['name'],
137+
});
138+
139+
expect(consoleLogSpy).toHaveBeenCalledTimes(2); // header + separator
140+
});
141+
142+
it('should display N/A for missing values', () => {
143+
const consoleLogSpy = vi.spyOn(console, 'log');
144+
145+
const data = [{ name: 'App1' }];
146+
147+
commons.displayTable(data, {
148+
headers: ['Name', 'Status'],
149+
columns: ['name', 'status'],
150+
columnWidth: 10,
151+
});
152+
153+
const calls = consoleLogSpy.mock.calls.flat();
154+
expect(calls.some(call => call.includes('N/A'))).toBe(true);
155+
});
156+
});
157+
158+
describe('showDiskSpaceUsage', () => {
159+
it('should display disk usage information', () => {
160+
const consoleLogSpy = vi.spyOn(console, 'log');
161+
162+
const data = {
163+
capacity: '1000000000',
164+
used: '500000000',
165+
};
166+
167+
commons.showDiskSpaceUsage(data);
168+
169+
expect(consoleLogSpy).toHaveBeenCalled();
170+
expect(mockFormatSize).toHaveBeenCalledWith('1000000000'); // capacity
171+
expect(mockFormatSize).toHaveBeenCalledWith('500000000'); // used
172+
expect(mockFormatSize).toHaveBeenCalledWith(500000000); // free space
173+
});
174+
175+
it('should calculate usage percentage correctly', () => {
176+
const consoleLogSpy = vi.spyOn(console, 'log');
177+
178+
const data = {
179+
capacity: '100',
180+
used: '25',
181+
};
182+
183+
commons.showDiskSpaceUsage(data);
184+
185+
const calls = consoleLogSpy.mock.calls.flat().join(' ');
186+
expect(calls).toContain('25.00%');
187+
});
188+
});
189+
190+
describe('resolvePath', () => {
191+
it('should resolve simple relative path', () => {
192+
expect(commons.resolvePath('/home/user', 'documents')).toBe('/home/user/documents');
193+
});
194+
195+
it('should resolve parent directory', () => {
196+
expect(commons.resolvePath('/home/user/documents', '..')).toBe('/home/user');
197+
});
198+
199+
it('should resolve current directory', () => {
200+
expect(commons.resolvePath('/home/user', '.')).toBe('/home/user');
201+
});
202+
203+
it('should resolve multiple parent directories', () => {
204+
expect(commons.resolvePath('/home/user/documents/files', '../..')).toBe('/home/user');
205+
});
206+
207+
it('should handle trailing slashes', () => {
208+
expect(commons.resolvePath('/home/user/', 'documents')).toBe('/home/user/documents');
209+
});
210+
211+
it('should handle empty relative path', () => {
212+
expect(commons.resolvePath('/home/user', '')).toBe('/home/user');
213+
});
214+
215+
it('should return root when going above root', () => {
216+
expect(commons.resolvePath('/home', '../../../..')).toBe('/');
217+
});
218+
219+
it('should normalize duplicate slashes', () => {
220+
expect(commons.resolvePath('/home//user', 'documents')).toBe('/home/user/documents');
221+
});
222+
});
223+
224+
describe('resolveRemotePath', () => {
225+
it('should return absolute path as-is', () => {
226+
expect(commons.resolveRemotePath('/home/user', '/absolute/path')).toBe('/absolute/path');
227+
});
228+
229+
it('should resolve relative path', () => {
230+
expect(commons.resolveRemotePath('/home/user', 'relative/path')).toBe('/home/user/relative/path');
231+
});
232+
});
233+
234+
describe('isValidAppName', () => {
235+
it('should return true for valid app name', () => {
236+
expect(commons.isValidAppName('my-app')).toBe(true);
237+
});
238+
239+
it('should return true for app name with spaces', () => {
240+
expect(commons.isValidAppName('my app')).toBe(true);
241+
});
242+
243+
it('should return false for empty string', () => {
244+
expect(commons.isValidAppName('')).toBe(false);
245+
});
246+
247+
it('should return false for whitespace only', () => {
248+
expect(commons.isValidAppName(' ')).toBe(false);
249+
});
250+
251+
it('should return false for reserved name "."', () => {
252+
expect(commons.isValidAppName('.')).toBe(false);
253+
});
254+
255+
it('should return false for reserved name ".."', () => {
256+
expect(commons.isValidAppName('..')).toBe(false);
257+
});
258+
259+
it('should return false for name with forward slash', () => {
260+
expect(commons.isValidAppName('my/app')).toBe(false);
261+
});
262+
263+
it('should return false for name with backslash', () => {
264+
expect(commons.isValidAppName('my\\app')).toBe(false);
265+
});
266+
267+
it('should return false for name with wildcard', () => {
268+
expect(commons.isValidAppName('my*app')).toBe(false);
269+
});
270+
271+
it('should return false for non-string input', () => {
272+
expect(commons.isValidAppName(123)).toBe(false);
273+
expect(commons.isValidAppName(null)).toBe(false);
274+
expect(commons.isValidAppName(undefined)).toBe(false);
275+
});
276+
});
277+
278+
describe('getDefaultHomePage', () => {
279+
it('should generate HTML with app name', () => {
280+
const html = commons.getDefaultHomePage('TestApp');
281+
282+
expect(html).toContain('<title>TestApp</title>');
283+
expect(html).toContain('Welcome to TestApp!');
284+
});
285+
286+
it('should include CSS files when provided', () => {
287+
const html = commons.getDefaultHomePage('TestApp', [], ['style.css', 'theme.css']);
288+
289+
expect(html).toContain('<link href="style.css" rel="stylesheet">');
290+
expect(html).toContain('<link href="theme.css" rel="stylesheet">');
291+
});
292+
293+
it('should include JS files when provided', () => {
294+
const html = commons.getDefaultHomePage('TestApp', ['app.js', 'utils.js']);
295+
296+
expect(html).toContain('<script type="text/babel" src="app.js"></script>');
297+
expect(html).toContain('<script src="utils.js"></script>');
298+
});
299+
300+
it('should use id="root" when react is included', () => {
301+
const html = commons.getDefaultHomePage('TestApp', ['react.js']);
302+
303+
expect(html).toContain('id="root"');
304+
});
305+
306+
it('should use id="app" when no react', () => {
307+
const html = commons.getDefaultHomePage('TestApp', ['vanilla.js']);
308+
309+
expect(html).toContain('id="app"');
310+
});
311+
});
312+
313+
describe('getVersionFromPackage', () => {
314+
it('should return version from package.json', async () => {
315+
mockReadFile.mockResolvedValueOnce(JSON.stringify({ version: '1.2.3' }));
316+
317+
const version = await commons.getVersionFromPackage();
318+
319+
expect(version).toBe('1.2.3');
320+
});
321+
322+
it('should fallback to production package.json on dev error', async () => {
323+
mockReadFile
324+
.mockRejectedValueOnce(new Error('File not found'))
325+
.mockResolvedValueOnce(JSON.stringify({ version: '2.0.0' }));
326+
327+
const version = await commons.getVersionFromPackage();
328+
329+
expect(version).toBe('2.0.0');
330+
});
331+
332+
it('should return null on error', async () => {
333+
mockReadFile.mockRejectedValue(new Error('Read error'));
334+
335+
const version = await commons.getVersionFromPackage();
336+
337+
expect(version).toBeNull();
338+
});
339+
});
340+
341+
describe('getLatestVersion', () => {
342+
beforeEach(() => {
343+
vi.stubGlobal('fetch', vi.fn());
344+
});
345+
346+
afterEach(() => {
347+
vi.unstubAllGlobals();
348+
});
349+
350+
it('should return up-to-date status when versions match', async () => {
351+
mockReadFile.mockResolvedValueOnce(JSON.stringify({ version: '1.0.0' }));
352+
global.fetch.mockResolvedValueOnce({
353+
ok: true,
354+
json: () => Promise.resolve({ version: '1.0.0' }),
355+
});
356+
357+
const result = await commons.getLatestVersion('puter-cli');
358+
359+
expect(result).toBe('v1.0.0 (up-to-date)');
360+
});
361+
362+
it('should return latest version when different', async () => {
363+
mockReadFile.mockResolvedValueOnce(JSON.stringify({ version: '1.0.0' }));
364+
global.fetch.mockResolvedValueOnce({
365+
ok: true,
366+
json: () => Promise.resolve({ version: '2.0.0' }),
367+
});
368+
369+
const result = await commons.getLatestVersion('puter-cli');
370+
371+
expect(result).toBe('v1.0.0 (latest: 2.0.0)');
372+
});
373+
374+
it('should return offline status when fetch fails', async () => {
375+
mockReadFile.mockResolvedValueOnce(JSON.stringify({ version: '1.0.0' }));
376+
global.fetch.mockRejectedValueOnce(new Error('Network error'));
377+
378+
const result = await commons.getLatestVersion('puter-cli');
379+
380+
expect(result).toBe('v1.0.0 (offline)');
381+
});
382+
383+
it('should handle unknown current version', async () => {
384+
mockReadFile.mockRejectedValue(new Error('Read error'));
385+
global.fetch.mockResolvedValueOnce({
386+
ok: true,
387+
json: () => Promise.resolve({ version: '2.0.0' }),
388+
});
389+
390+
const result = await commons.getLatestVersion('puter-cli');
391+
392+
expect(result).toBe('vunknown (latest: 2.0.0)');
393+
});
394+
});

0 commit comments

Comments
 (0)