portok 1.0.5 → 1.0.6
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/README.md +5 -1
- package/package.json +1 -1
- package/portok.js +148 -5
- package/test/init.test.js +269 -0
package/README.md
CHANGED
|
@@ -195,9 +195,10 @@ curl -H "x-admin-token: your-token" http://127.0.0.1:3000/__health
|
|
|
195
195
|
portok <command> [options]
|
|
196
196
|
|
|
197
197
|
Management Commands:
|
|
198
|
-
init Initialize portok
|
|
198
|
+
init Initialize portok (creates dirs, installs systemd service)
|
|
199
199
|
add <name> Create a new service instance
|
|
200
200
|
remove <name> Remove a service instance (stops, disables, deletes config/state)
|
|
201
|
+
clean Remove ALL portok data (configs, states, systemd service)
|
|
201
202
|
list List all configured instances and their status
|
|
202
203
|
|
|
203
204
|
Service Control Commands:
|
|
@@ -231,6 +232,9 @@ Options for 'remove' command:
|
|
|
231
232
|
--force Skip confirmation prompt
|
|
232
233
|
--keep-state Keep state file (/var/lib/portok/<name>.json)
|
|
233
234
|
|
|
235
|
+
Options for 'clean' command:
|
|
236
|
+
--force Skip confirmation prompt (required)
|
|
237
|
+
|
|
234
238
|
Options for 'logs' command:
|
|
235
239
|
--follow, -f Follow log output
|
|
236
240
|
--lines, -n Number of lines to show (default: 50)
|
package/package.json
CHANGED
package/portok.js
CHANGED
|
@@ -316,12 +316,26 @@ async function cmdInit(options) {
|
|
|
316
316
|
const stateDir = '/var/lib/portok';
|
|
317
317
|
const systemdDir = '/etc/systemd/system';
|
|
318
318
|
const serviceFileName = 'portok@.service';
|
|
319
|
+
const { execSync } = require('child_process');
|
|
319
320
|
|
|
320
|
-
|
|
321
|
+
// Detect current user for service configuration
|
|
322
|
+
let currentUser = 'root';
|
|
323
|
+
let currentGroup = 'root';
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
currentUser = execSync('whoami', { encoding: 'utf-8' }).trim();
|
|
327
|
+
currentGroup = execSync(`id -gn ${currentUser}`, { encoding: 'utf-8' }).trim();
|
|
328
|
+
} catch (e) {
|
|
329
|
+
// Fallback to root if detection fails
|
|
330
|
+
}
|
|
321
331
|
|
|
322
|
-
// Check if running as root (needed for /etc and /var/lib)
|
|
323
332
|
const isRoot = process.getuid && process.getuid() === 0;
|
|
324
333
|
|
|
334
|
+
console.log(`${colors.bold}Initializing Portok...${colors.reset}\n`);
|
|
335
|
+
console.log(` User: ${colors.cyan}${currentUser}${colors.reset}`);
|
|
336
|
+
console.log(` Group: ${colors.cyan}${currentGroup}${colors.reset}`);
|
|
337
|
+
console.log(` Node: ${colors.dim}${process.execPath}${colors.reset}\n`);
|
|
338
|
+
|
|
325
339
|
if (!isRoot && !options.force) {
|
|
326
340
|
console.log(`${colors.yellow}Warning:${colors.reset} This command typically requires root privileges.`);
|
|
327
341
|
console.log(`Run with sudo or use --force to attempt anyway.\n`);
|
|
@@ -394,6 +408,10 @@ async function cmdInit(options) {
|
|
|
394
408
|
serviceContent = serviceContent.replace(/WorkingDirectory=\/opt\/portok/g, `WorkingDirectory=${portokDir}`);
|
|
395
409
|
serviceContent = serviceContent.replace(/\/opt\/portok\/portokd\.js/g, `${portokDir}/portokd.js`);
|
|
396
410
|
|
|
411
|
+
// Replace user/group with detected user (important for nvm/fnm compatibility)
|
|
412
|
+
serviceContent = serviceContent.replace(/User=www-data/g, `User=${currentUser}`);
|
|
413
|
+
serviceContent = serviceContent.replace(/Group=www-data/g, `Group=${currentGroup}`);
|
|
414
|
+
|
|
397
415
|
const existingContent = fs.existsSync(systemdDest) ? fs.readFileSync(systemdDest, 'utf-8') : null;
|
|
398
416
|
|
|
399
417
|
if (existingContent && existingContent.trim() === serviceContent.trim()) {
|
|
@@ -404,7 +422,6 @@ async function cmdInit(options) {
|
|
|
404
422
|
|
|
405
423
|
// Reload systemd daemon
|
|
406
424
|
try {
|
|
407
|
-
const { execSync } = require('child_process');
|
|
408
425
|
execSync('systemctl daemon-reload', { stdio: 'pipe' });
|
|
409
426
|
results.push({ path: 'systemctl daemon-reload', status: 'created' });
|
|
410
427
|
} catch (e) {
|
|
@@ -687,6 +704,125 @@ async function cmdRemove(name, options) {
|
|
|
687
704
|
return 0;
|
|
688
705
|
}
|
|
689
706
|
|
|
707
|
+
async function cmdClean(options) {
|
|
708
|
+
const configDir = ENV_FILE_DIR;
|
|
709
|
+
const stateDir = '/var/lib/portok';
|
|
710
|
+
const systemdDir = '/etc/systemd/system';
|
|
711
|
+
const serviceFileName = 'portok@.service';
|
|
712
|
+
const systemdDest = path.join(systemdDir, serviceFileName);
|
|
713
|
+
|
|
714
|
+
// Check if running as root
|
|
715
|
+
const isRoot = process.getuid && process.getuid() === 0;
|
|
716
|
+
|
|
717
|
+
if (!isRoot && !options.force) {
|
|
718
|
+
console.error(`${colors.red}Error:${colors.reset} This command requires root privileges.`);
|
|
719
|
+
console.error('Run with: sudo portok clean');
|
|
720
|
+
return 1;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Confirmation (unless --force)
|
|
724
|
+
if (!options.force && !options.json) {
|
|
725
|
+
console.log(`${colors.yellow}Warning:${colors.reset} This will remove ALL portok data:\n`);
|
|
726
|
+
console.log(` • ${configDir}/ (all service configs)`);
|
|
727
|
+
console.log(` • ${stateDir}/ (all state files)`);
|
|
728
|
+
console.log(` • ${systemdDest}`);
|
|
729
|
+
console.log(`\nUse ${colors.cyan}--force${colors.reset} to confirm.\n`);
|
|
730
|
+
return 1;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const { execSync } = require('child_process');
|
|
734
|
+
const results = [];
|
|
735
|
+
|
|
736
|
+
// 1. Stop and disable all running portok services
|
|
737
|
+
try {
|
|
738
|
+
const output = execSync('systemctl list-units --type=service --all | grep "portok@" | awk \'{print $1}\'', { encoding: 'utf-8' });
|
|
739
|
+
const services = output.trim().split('\n').filter(s => s);
|
|
740
|
+
|
|
741
|
+
for (const service of services) {
|
|
742
|
+
try {
|
|
743
|
+
execSync(`systemctl stop ${service}`, { stdio: 'pipe' });
|
|
744
|
+
execSync(`systemctl disable ${service}`, { stdio: 'pipe' });
|
|
745
|
+
results.push({ action: `stop ${service}`, status: 'success' });
|
|
746
|
+
} catch (e) {
|
|
747
|
+
results.push({ action: `stop ${service}`, status: 'skipped' });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
} catch (e) {
|
|
751
|
+
// No services found, that's ok
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// 2. Remove systemd service file
|
|
755
|
+
if (fs.existsSync(systemdDest)) {
|
|
756
|
+
try {
|
|
757
|
+
fs.unlinkSync(systemdDest);
|
|
758
|
+
results.push({ action: 'remove systemd service', status: 'success', path: systemdDest });
|
|
759
|
+
|
|
760
|
+
// Reload systemd
|
|
761
|
+
try {
|
|
762
|
+
execSync('systemctl daemon-reload', { stdio: 'pipe' });
|
|
763
|
+
} catch (e) {}
|
|
764
|
+
} catch (err) {
|
|
765
|
+
results.push({ action: 'remove systemd service', status: 'error', error: err.message });
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
results.push({ action: 'remove systemd service', status: 'skipped' });
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// 3. Remove config directory
|
|
772
|
+
if (fs.existsSync(configDir)) {
|
|
773
|
+
try {
|
|
774
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
775
|
+
results.push({ action: 'remove config dir', status: 'success', path: configDir });
|
|
776
|
+
} catch (err) {
|
|
777
|
+
results.push({ action: 'remove config dir', status: 'error', error: err.message });
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
results.push({ action: 'remove config dir', status: 'skipped' });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// 4. Remove state directory
|
|
784
|
+
if (fs.existsSync(stateDir)) {
|
|
785
|
+
try {
|
|
786
|
+
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
787
|
+
results.push({ action: 'remove state dir', status: 'success', path: stateDir });
|
|
788
|
+
} catch (err) {
|
|
789
|
+
results.push({ action: 'remove state dir', status: 'error', error: err.message });
|
|
790
|
+
}
|
|
791
|
+
} else {
|
|
792
|
+
results.push({ action: 'remove state dir', status: 'skipped' });
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const hasError = results.some(r => r.status === 'error');
|
|
796
|
+
|
|
797
|
+
if (options.json) {
|
|
798
|
+
console.log(JSON.stringify({ success: !hasError, results }, null, 2));
|
|
799
|
+
return hasError ? 1 : 0;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Display results
|
|
803
|
+
console.log(`\n${colors.bold}Cleaning Portok...${colors.reset}\n`);
|
|
804
|
+
|
|
805
|
+
for (const r of results) {
|
|
806
|
+
const icon = r.status === 'success' ? `${colors.green}✓${colors.reset}` :
|
|
807
|
+
r.status === 'skipped' ? `${colors.dim}○${colors.reset}` :
|
|
808
|
+
`${colors.red}✗${colors.reset}`;
|
|
809
|
+
|
|
810
|
+
const statusLabel = r.status === 'success' ? (r.path ? `Removed ${r.path}` : 'Done') :
|
|
811
|
+
r.status === 'skipped' ? 'Skipped (not found)' :
|
|
812
|
+
`Error: ${r.error}`;
|
|
813
|
+
|
|
814
|
+
console.log(` ${icon} ${r.action.padEnd(24)} ${statusLabel}`);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (hasError) {
|
|
818
|
+
console.log(`\n${colors.yellow}Warning:${colors.reset} Some operations failed.`);
|
|
819
|
+
return 1;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
console.log(`\n${colors.green}✓ Portok cleaned successfully!${colors.reset}`);
|
|
823
|
+
return 0;
|
|
824
|
+
}
|
|
825
|
+
|
|
690
826
|
async function cmdList(options) {
|
|
691
827
|
const configDir = ENV_FILE_DIR;
|
|
692
828
|
|
|
@@ -918,9 +1054,10 @@ ${colors.bold}USAGE${colors.reset}
|
|
|
918
1054
|
|
|
919
1055
|
${colors.bold}COMMANDS${colors.reset}
|
|
920
1056
|
${colors.cyan}Management:${colors.reset}
|
|
921
|
-
init Initialize portok
|
|
1057
|
+
init Initialize portok (creates dirs, installs systemd service)
|
|
922
1058
|
add <name> Create a new service instance
|
|
923
1059
|
remove <name> Remove a service instance (config + state)
|
|
1060
|
+
clean Remove ALL portok data (configs, states, systemd service)
|
|
924
1061
|
list List all configured instances and their status
|
|
925
1062
|
|
|
926
1063
|
${colors.cyan}Service Control:${colors.reset}
|
|
@@ -954,6 +1091,9 @@ ${colors.bold}OPTIONS${colors.reset}
|
|
|
954
1091
|
--force Skip confirmation prompt
|
|
955
1092
|
--keep-state Keep state file (/var/lib/portok/<name>.json)
|
|
956
1093
|
|
|
1094
|
+
${colors.dim}For 'clean' command:${colors.reset}
|
|
1095
|
+
--force Skip confirmation prompt (required)
|
|
1096
|
+
|
|
957
1097
|
${colors.dim}For 'logs' command:${colors.reset}
|
|
958
1098
|
--follow, -f Follow log output
|
|
959
1099
|
--lines, -n Number of lines to show (default: 50)
|
|
@@ -1013,7 +1153,7 @@ async function main() {
|
|
|
1013
1153
|
}
|
|
1014
1154
|
|
|
1015
1155
|
// Management commands (don't require daemon connection)
|
|
1016
|
-
const managementCommands = ['init', 'add', 'remove', 'list', 'start', 'stop', 'restart', 'enable', 'disable', 'logs'];
|
|
1156
|
+
const managementCommands = ['init', 'add', 'remove', 'clean', 'list', 'start', 'stop', 'restart', 'enable', 'disable', 'logs'];
|
|
1017
1157
|
|
|
1018
1158
|
if (managementCommands.includes(args.command)) {
|
|
1019
1159
|
let exitCode = 1;
|
|
@@ -1027,6 +1167,9 @@ async function main() {
|
|
|
1027
1167
|
case 'remove':
|
|
1028
1168
|
exitCode = await cmdRemove(args.positional[0], args.options);
|
|
1029
1169
|
break;
|
|
1170
|
+
case 'clean':
|
|
1171
|
+
exitCode = await cmdClean(args.options);
|
|
1172
|
+
break;
|
|
1030
1173
|
case 'list':
|
|
1031
1174
|
exitCode = await cmdList(args.options);
|
|
1032
1175
|
break;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init Command Tests
|
|
3
|
+
* Tests user detection, node path detection, and service template replacement
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, it, before, after, beforeEach, afterEach } = require('node:test');
|
|
7
|
+
const assert = require('node:assert');
|
|
8
|
+
const { spawn, execSync } = require('node:child_process');
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
|
|
13
|
+
// Test directory for temporary files
|
|
14
|
+
const TEST_DIR = path.join(os.tmpdir(), `portok-init-test-${Date.now()}`);
|
|
15
|
+
const TEST_CONFIG_DIR = path.join(TEST_DIR, 'etc', 'portok');
|
|
16
|
+
const TEST_STATE_DIR = path.join(TEST_DIR, 'var', 'lib', 'portok');
|
|
17
|
+
const TEST_SYSTEMD_DIR = path.join(TEST_DIR, 'etc', 'systemd', 'system');
|
|
18
|
+
|
|
19
|
+
describe('Init Command', () => {
|
|
20
|
+
before(() => {
|
|
21
|
+
// Create test directories
|
|
22
|
+
fs.mkdirSync(TEST_SYSTEMD_DIR, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
after(() => {
|
|
26
|
+
// Cleanup test directories
|
|
27
|
+
try {
|
|
28
|
+
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// Ignore cleanup errors
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('User Detection', () => {
|
|
35
|
+
it('should detect current user via whoami', () => {
|
|
36
|
+
const whoami = execSync('whoami', { encoding: 'utf-8' }).trim();
|
|
37
|
+
assert.ok(whoami.length > 0, 'whoami should return a user');
|
|
38
|
+
|
|
39
|
+
// In Docker Alpine, this is typically 'root'
|
|
40
|
+
console.log(` Detected user: ${whoami}`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should detect current user group via id -gn', () => {
|
|
44
|
+
const user = execSync('whoami', { encoding: 'utf-8' }).trim();
|
|
45
|
+
const group = execSync(`id -gn ${user}`, { encoding: 'utf-8' }).trim();
|
|
46
|
+
assert.ok(group.length > 0, 'id -gn should return a group');
|
|
47
|
+
|
|
48
|
+
console.log(` Detected group: ${group}`);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should have process.getuid() available', () => {
|
|
52
|
+
assert.ok(typeof process.getuid === 'function', 'process.getuid should be a function');
|
|
53
|
+
const uid = process.getuid();
|
|
54
|
+
assert.ok(typeof uid === 'number', 'getuid should return a number');
|
|
55
|
+
|
|
56
|
+
console.log(` UID: ${uid}`);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Node Path Detection', () => {
|
|
61
|
+
it('should detect node executable path via process.execPath', () => {
|
|
62
|
+
const nodePath = process.execPath;
|
|
63
|
+
assert.ok(nodePath.length > 0, 'process.execPath should not be empty');
|
|
64
|
+
assert.ok(fs.existsSync(nodePath), 'node executable should exist');
|
|
65
|
+
|
|
66
|
+
console.log(` Node path: ${nodePath}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should work with nvm-style paths', () => {
|
|
70
|
+
const nodePath = process.execPath;
|
|
71
|
+
|
|
72
|
+
// Node path should be a valid executable
|
|
73
|
+
const version = execSync(`"${nodePath}" --version`, { encoding: 'utf-8' }).trim();
|
|
74
|
+
assert.ok(version.startsWith('v'), 'Should be able to get node version');
|
|
75
|
+
|
|
76
|
+
console.log(` Node version: ${version}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should not be hardcoded /usr/bin/node when using nvm', () => {
|
|
80
|
+
const nodePath = process.execPath;
|
|
81
|
+
|
|
82
|
+
// In a proper setup, this could be /usr/bin/node, /usr/local/bin/node,
|
|
83
|
+
// or ~/.nvm/versions/node/vX.X.X/bin/node
|
|
84
|
+
// We just verify it's a valid path
|
|
85
|
+
assert.ok(
|
|
86
|
+
nodePath.includes('node'),
|
|
87
|
+
'Path should contain "node"'
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('Service Template Replacement', () => {
|
|
93
|
+
it('should read portok@.service template', () => {
|
|
94
|
+
const serviceFile = path.join(__dirname, '..', 'portok@.service');
|
|
95
|
+
assert.ok(fs.existsSync(serviceFile), 'portok@.service should exist');
|
|
96
|
+
|
|
97
|
+
const content = fs.readFileSync(serviceFile, 'utf-8');
|
|
98
|
+
assert.ok(content.includes('[Unit]'), 'Should be a valid systemd unit');
|
|
99
|
+
assert.ok(content.includes('[Service]'), 'Should have Service section');
|
|
100
|
+
assert.ok(content.includes('[Install]'), 'Should have Install section');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should have replaceable placeholders in template', () => {
|
|
104
|
+
const serviceFile = path.join(__dirname, '..', 'portok@.service');
|
|
105
|
+
const content = fs.readFileSync(serviceFile, 'utf-8');
|
|
106
|
+
|
|
107
|
+
// Check for placeholder patterns that will be replaced
|
|
108
|
+
assert.ok(content.includes('/usr/bin/node'), 'Should have node path placeholder');
|
|
109
|
+
assert.ok(content.includes('/opt/portok'), 'Should have portok dir placeholder');
|
|
110
|
+
assert.ok(content.includes('User=www-data'), 'Should have user placeholder');
|
|
111
|
+
assert.ok(content.includes('Group=www-data'), 'Should have group placeholder');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should correctly replace node path', () => {
|
|
115
|
+
const serviceFile = path.join(__dirname, '..', 'portok@.service');
|
|
116
|
+
let content = fs.readFileSync(serviceFile, 'utf-8');
|
|
117
|
+
|
|
118
|
+
const nodePath = process.execPath;
|
|
119
|
+
content = content.replace(/\/usr\/bin\/node/g, nodePath);
|
|
120
|
+
|
|
121
|
+
assert.ok(content.includes(nodePath), 'Node path should be replaced');
|
|
122
|
+
assert.ok(!content.includes('/usr/bin/node'), 'Old node path should be gone');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should correctly replace working directory', () => {
|
|
126
|
+
const serviceFile = path.join(__dirname, '..', 'portok@.service');
|
|
127
|
+
let content = fs.readFileSync(serviceFile, 'utf-8');
|
|
128
|
+
|
|
129
|
+
const portokDir = path.dirname(serviceFile);
|
|
130
|
+
content = content.replace(/WorkingDirectory=\/opt\/portok/g, `WorkingDirectory=${portokDir}`);
|
|
131
|
+
content = content.replace(/\/opt\/portok\/portokd\.js/g, `${portokDir}/portokd.js`);
|
|
132
|
+
|
|
133
|
+
assert.ok(content.includes(portokDir), 'Portok dir should be replaced');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should correctly replace user and group', () => {
|
|
137
|
+
const serviceFile = path.join(__dirname, '..', 'portok@.service');
|
|
138
|
+
let content = fs.readFileSync(serviceFile, 'utf-8');
|
|
139
|
+
|
|
140
|
+
const user = execSync('whoami', { encoding: 'utf-8' }).trim();
|
|
141
|
+
const group = execSync(`id -gn ${user}`, { encoding: 'utf-8' }).trim();
|
|
142
|
+
|
|
143
|
+
content = content.replace(/User=www-data/g, `User=${user}`);
|
|
144
|
+
content = content.replace(/Group=www-data/g, `Group=${group}`);
|
|
145
|
+
|
|
146
|
+
assert.ok(content.includes(`User=${user}`), 'User should be replaced');
|
|
147
|
+
assert.ok(content.includes(`Group=${group}`), 'Group should be replaced');
|
|
148
|
+
assert.ok(!content.includes('www-data'), 'www-data should be gone');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should produce valid systemd unit after all replacements', () => {
|
|
152
|
+
const serviceFile = path.join(__dirname, '..', 'portok@.service');
|
|
153
|
+
let content = fs.readFileSync(serviceFile, 'utf-8');
|
|
154
|
+
|
|
155
|
+
const nodePath = process.execPath;
|
|
156
|
+
const portokDir = path.dirname(serviceFile);
|
|
157
|
+
const user = execSync('whoami', { encoding: 'utf-8' }).trim();
|
|
158
|
+
const group = execSync(`id -gn ${user}`, { encoding: 'utf-8' }).trim();
|
|
159
|
+
|
|
160
|
+
// Apply all replacements
|
|
161
|
+
content = content.replace(/\/usr\/bin\/node/g, nodePath);
|
|
162
|
+
content = content.replace(/WorkingDirectory=\/opt\/portok/g, `WorkingDirectory=${portokDir}`);
|
|
163
|
+
content = content.replace(/\/opt\/portok\/portokd\.js/g, `${portokDir}/portokd.js`);
|
|
164
|
+
content = content.replace(/User=www-data/g, `User=${user}`);
|
|
165
|
+
content = content.replace(/Group=www-data/g, `Group=${group}`);
|
|
166
|
+
|
|
167
|
+
// Verify structure
|
|
168
|
+
assert.ok(content.includes('[Unit]'), 'Should have Unit section');
|
|
169
|
+
assert.ok(content.includes('[Service]'), 'Should have Service section');
|
|
170
|
+
assert.ok(content.includes('[Install]'), 'Should have Install section');
|
|
171
|
+
assert.ok(content.includes(`ExecStart=${nodePath}`), 'Should have correct ExecStart');
|
|
172
|
+
assert.ok(content.includes(`User=${user}`), 'Should have correct User');
|
|
173
|
+
assert.ok(content.includes(`Group=${group}`), 'Should have correct Group');
|
|
174
|
+
|
|
175
|
+
// Write to test file for inspection
|
|
176
|
+
const testServiceFile = path.join(TEST_SYSTEMD_DIR, 'portok@.service');
|
|
177
|
+
fs.writeFileSync(testServiceFile, content);
|
|
178
|
+
|
|
179
|
+
console.log(` Generated service file: ${testServiceFile}`);
|
|
180
|
+
console.log(` ExecStart: ${nodePath} ${portokDir}/portokd.js`);
|
|
181
|
+
console.log(` User: ${user}, Group: ${group}`);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('Root User Handling', () => {
|
|
186
|
+
it('should detect if running as root', () => {
|
|
187
|
+
const isRoot = process.getuid && process.getuid() === 0;
|
|
188
|
+
console.log(` Running as root: ${isRoot}`);
|
|
189
|
+
|
|
190
|
+
// In Docker, we typically run as root
|
|
191
|
+
if (process.env.CI || process.env.DOCKER) {
|
|
192
|
+
assert.strictEqual(isRoot, true, 'Should be root in Docker/CI');
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should set User=root when running as root', () => {
|
|
197
|
+
const isRoot = process.getuid && process.getuid() === 0;
|
|
198
|
+
|
|
199
|
+
if (isRoot) {
|
|
200
|
+
const user = execSync('whoami', { encoding: 'utf-8' }).trim();
|
|
201
|
+
assert.strictEqual(user, 'root', 'whoami should return root');
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle nvm paths in root home directory', () => {
|
|
206
|
+
const nodePath = process.execPath;
|
|
207
|
+
const isRoot = process.getuid && process.getuid() === 0;
|
|
208
|
+
|
|
209
|
+
// NVM typically installs to ~/.nvm which for root is /root/.nvm
|
|
210
|
+
// But in Docker Alpine, it's usually /usr/local/bin/node
|
|
211
|
+
console.log(` Node in root home: ${nodePath.startsWith('/root/')}`);
|
|
212
|
+
console.log(` Node path: ${nodePath}`);
|
|
213
|
+
|
|
214
|
+
// The important thing is that the path is accessible
|
|
215
|
+
assert.ok(fs.existsSync(nodePath), 'Node path should be accessible');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('Init Output Format', () => {
|
|
220
|
+
it('should display user info in init output', async () => {
|
|
221
|
+
// Run portok.js with a mock to capture output
|
|
222
|
+
const portokPath = path.join(__dirname, '..', 'portok.js');
|
|
223
|
+
|
|
224
|
+
// Just verify the script exists and is executable
|
|
225
|
+
assert.ok(fs.existsSync(portokPath), 'portok.js should exist');
|
|
226
|
+
|
|
227
|
+
const stats = fs.statSync(portokPath);
|
|
228
|
+
assert.ok(stats.mode & 0o111, 'portok.js should be executable');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('Clean Command', () => {
|
|
234
|
+
const CLEAN_TEST_DIR = path.join(os.tmpdir(), `portok-clean-test-${Date.now()}`);
|
|
235
|
+
const CLEAN_CONFIG_DIR = path.join(CLEAN_TEST_DIR, 'etc', 'portok');
|
|
236
|
+
const CLEAN_STATE_DIR = path.join(CLEAN_TEST_DIR, 'var', 'lib', 'portok');
|
|
237
|
+
|
|
238
|
+
before(() => {
|
|
239
|
+
// Create test directories with some files
|
|
240
|
+
fs.mkdirSync(CLEAN_CONFIG_DIR, { recursive: true });
|
|
241
|
+
fs.mkdirSync(CLEAN_STATE_DIR, { recursive: true });
|
|
242
|
+
fs.writeFileSync(path.join(CLEAN_CONFIG_DIR, 'test.env'), 'TEST=1');
|
|
243
|
+
fs.writeFileSync(path.join(CLEAN_STATE_DIR, 'test.json'), '{}');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
after(() => {
|
|
247
|
+
try {
|
|
248
|
+
fs.rmSync(CLEAN_TEST_DIR, { recursive: true, force: true });
|
|
249
|
+
} catch (e) {}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should require --force flag', () => {
|
|
253
|
+
// The clean command should require --force
|
|
254
|
+
// This is just a logical test - actual command testing would need sudo
|
|
255
|
+
assert.ok(true, 'Clean command requires --force flag');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should be able to remove directories recursively', () => {
|
|
259
|
+
// Test fs.rmSync works
|
|
260
|
+
const testDir = path.join(CLEAN_TEST_DIR, 'rmtest');
|
|
261
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
262
|
+
fs.writeFileSync(path.join(testDir, 'file.txt'), 'test');
|
|
263
|
+
|
|
264
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
265
|
+
|
|
266
|
+
assert.ok(!fs.existsSync(testDir), 'Directory should be removed');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|