@wangyaoshen/remux 0.3.8-dev.a8ceb0c
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/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- package/.github/dependabot.yml +33 -0
- package/.github/workflows/ci.yml +65 -0
- package/.github/workflows/deploy.yml +65 -0
- package/.github/workflows/publish.yml +312 -0
- package/.github/workflows/release-please.yml +21 -0
- package/.gitmodules +3 -0
- package/.nvmrc +1 -0
- package/.release-please-manifest.json +3 -0
- package/CLAUDE.md +104 -0
- package/Dockerfile +23 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/apps/ios/Config/signing.xcconfig +4 -0
- package/apps/ios/Package.swift +26 -0
- package/apps/ios/Remux.xcodeproj/project.pbxproj +477 -0
- package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
- package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
- package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
- package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
- package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
- package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
- package/apps/ios/Sources/Remux/RootView.swift +130 -0
- package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
- package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
- package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
- package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
- package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
- package/apps/macos/Package.swift +37 -0
- package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
- package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
- package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
- package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
- package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
- package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
- package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
- package/apps/macos/Resources/terminfo/67/ghostty +0 -0
- package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
- package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
- package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
- package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
- package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
- package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
- package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
- package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
- package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
- package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
- package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
- package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
- package/apps/macos/Sources/Remux/SocketController.swift +258 -0
- package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
- package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
- package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
- package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
- package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
- package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
- package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
- package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
- package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
- package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
- package/build.mjs +33 -0
- package/native/android/DecodeGoldenPayloads.kt +487 -0
- package/native/android/ProtocolModels.kt +188 -0
- package/native/ios/DecodeGoldenPayloads.swift +711 -0
- package/native/ios/ProtocolModels.swift +200 -0
- package/package.json +45 -0
- package/packages/RemuxKit/Package.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
- package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
- package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
- package/playwright.config.ts +17 -0
- package/pnpm-lock.yaml +1588 -0
- package/pty-daemon.js +303 -0
- package/release-please-config.json +14 -0
- package/scripts/auto-deploy.sh +46 -0
- package/scripts/build-dmg.sh +121 -0
- package/scripts/build-ghostty-kit.sh +43 -0
- package/scripts/check-active-terminology.mjs +132 -0
- package/scripts/setup-ci-secrets.sh +80 -0
- package/scripts/sync-ghostty-web.sh +28 -0
- package/scripts/upload-testflight.sh +100 -0
- package/server.js +7074 -0
- package/src/adapters/agent-events.ts +246 -0
- package/src/adapters/claude-code.ts +158 -0
- package/src/adapters/codex.ts +210 -0
- package/src/adapters/generic-shell.ts +58 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +99 -0
- package/src/adapters/types.ts +41 -0
- package/src/auth.ts +174 -0
- package/src/e2ee.ts +236 -0
- package/src/git-service.ts +168 -0
- package/src/message-buffer.ts +137 -0
- package/src/pty-daemon.ts +357 -0
- package/src/push.ts +127 -0
- package/src/renderers.ts +455 -0
- package/src/server.ts +2407 -0
- package/src/service.ts +226 -0
- package/src/session.ts +978 -0
- package/src/store.ts +1422 -0
- package/src/team.ts +123 -0
- package/src/tunnel.ts +126 -0
- package/src/types.d.ts +50 -0
- package/src/vt-tracker.ts +188 -0
- package/src/workspace-head.ts +144 -0
- package/src/workspace.ts +153 -0
- package/src/ws-handler.ts +1526 -0
- package/start.ps1 +83 -0
- package/tests/adapters.test.js +171 -0
- package/tests/auth.test.js +243 -0
- package/tests/codex-adapter.test.js +535 -0
- package/tests/durable-stream.test.js +153 -0
- package/tests/e2e/app.spec.js +530 -0
- package/tests/e2ee.test.js +325 -0
- package/tests/message-buffer.test.js +245 -0
- package/tests/message-routing.test.js +305 -0
- package/tests/pty-daemon.test.js +346 -0
- package/tests/push.test.js +281 -0
- package/tests/renderers.test.js +391 -0
- package/tests/search-shell.test.js +499 -0
- package/tests/server.test.js +882 -0
- package/tests/service.test.js +267 -0
- package/tests/store.test.js +369 -0
- package/tests/tunnel.test.js +67 -0
- package/tests/workspace-head.test.js +116 -0
- package/tests/workspace.test.js +417 -0
- package/tsconfig.backend.json +11 -0
- package/tsconfig.json +15 -0
- package/tui/client/client_test.go +125 -0
- package/tui/client/connection.go +342 -0
- package/tui/client/host_manager.go +141 -0
- package/tui/config/cache.go +81 -0
- package/tui/config/config.go +53 -0
- package/tui/config/config_test.go +89 -0
- package/tui/go.mod +32 -0
- package/tui/go.sum +50 -0
- package/tui/main.go +261 -0
- package/tui/tests/integration_test.go +283 -0
- package/tui/ui/model.go +310 -0
- package/vitest.config.js +10 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for launchd service management (install/uninstall/status).
|
|
3
|
+
* Tests plist generation, path handling, and launchctl interaction.
|
|
4
|
+
* Mocks fs and child_process to avoid side effects.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
|
|
11
|
+
// We'll dynamically import the module after mocking
|
|
12
|
+
let service;
|
|
13
|
+
let mockFs;
|
|
14
|
+
let mockExecSync;
|
|
15
|
+
|
|
16
|
+
const PLIST_PATH = path.join(
|
|
17
|
+
homedir(),
|
|
18
|
+
"Library",
|
|
19
|
+
"LaunchAgents",
|
|
20
|
+
"com.remux.agent.plist",
|
|
21
|
+
);
|
|
22
|
+
const LOG_DIR = path.join(homedir(), ".remux", "logs");
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
vi.resetModules();
|
|
26
|
+
|
|
27
|
+
// Mock fs
|
|
28
|
+
mockFs = {
|
|
29
|
+
existsSync: vi.fn().mockReturnValue(false),
|
|
30
|
+
writeFileSync: vi.fn(),
|
|
31
|
+
unlinkSync: vi.fn(),
|
|
32
|
+
mkdirSync: vi.fn(),
|
|
33
|
+
readFileSync: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
vi.doMock("fs", () => ({ default: mockFs, ...mockFs }));
|
|
36
|
+
|
|
37
|
+
// Mock child_process
|
|
38
|
+
mockExecSync = vi.fn().mockReturnValue("");
|
|
39
|
+
vi.doMock("child_process", () => ({
|
|
40
|
+
execSync: mockExecSync,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
service = await import("../src/service.ts");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("generatePlist", () => {
|
|
51
|
+
it("generates valid plist XML with correct structure", () => {
|
|
52
|
+
const xml = service.generatePlist({});
|
|
53
|
+
expect(xml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
|
|
54
|
+
expect(xml).toContain("<!DOCTYPE plist");
|
|
55
|
+
expect(xml).toContain('<plist version="1.0">');
|
|
56
|
+
expect(xml).toContain("<key>Label</key>");
|
|
57
|
+
expect(xml).toContain("<string>com.remux.agent</string>");
|
|
58
|
+
expect(xml).toContain("<key>RunAtLoad</key>");
|
|
59
|
+
expect(xml).toContain("<true/>");
|
|
60
|
+
expect(xml).toContain("<key>KeepAlive</key>");
|
|
61
|
+
expect(xml).toContain("<key>ProgramArguments</key>");
|
|
62
|
+
expect(xml).toContain("<key>StandardOutPath</key>");
|
|
63
|
+
expect(xml).toContain("<key>StandardErrorPath</key>");
|
|
64
|
+
expect(xml).toContain("</plist>");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("includes node and server.js in ProgramArguments", () => {
|
|
68
|
+
const xml = service.generatePlist({});
|
|
69
|
+
// Should contain the node executable path
|
|
70
|
+
expect(xml).toContain(process.execPath);
|
|
71
|
+
// Should contain server.js path
|
|
72
|
+
expect(xml).toContain("server.js");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("includes extra args in ProgramArguments", () => {
|
|
76
|
+
const xml = service.generatePlist({ args: ["--verbose", "--debug"] });
|
|
77
|
+
expect(xml).toContain("<string>--verbose</string>");
|
|
78
|
+
expect(xml).toContain("<string>--debug</string>");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("sets PORT in EnvironmentVariables when port option given", () => {
|
|
82
|
+
const xml = service.generatePlist({ port: 9999 });
|
|
83
|
+
expect(xml).toContain("<key>PORT</key>");
|
|
84
|
+
expect(xml).toContain("<string>9999</string>");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("includes REMUX_TOKEN from env if set", () => {
|
|
88
|
+
const origToken = process.env.REMUX_TOKEN;
|
|
89
|
+
process.env.REMUX_TOKEN = "my-secret-token";
|
|
90
|
+
try {
|
|
91
|
+
const xml = service.generatePlist({});
|
|
92
|
+
expect(xml).toContain("<key>REMUX_TOKEN</key>");
|
|
93
|
+
expect(xml).toContain("<string>my-secret-token</string>");
|
|
94
|
+
} finally {
|
|
95
|
+
if (origToken === undefined) delete process.env.REMUX_TOKEN;
|
|
96
|
+
else process.env.REMUX_TOKEN = origToken;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("omits REMUX_TOKEN when not set", () => {
|
|
101
|
+
const origToken = process.env.REMUX_TOKEN;
|
|
102
|
+
delete process.env.REMUX_TOKEN;
|
|
103
|
+
try {
|
|
104
|
+
const xml = service.generatePlist({});
|
|
105
|
+
expect(xml).not.toContain("<key>REMUX_TOKEN</key>");
|
|
106
|
+
} finally {
|
|
107
|
+
if (origToken !== undefined) process.env.REMUX_TOKEN = origToken;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("sets correct log paths", () => {
|
|
112
|
+
const xml = service.generatePlist({});
|
|
113
|
+
expect(xml).toContain(path.join(LOG_DIR, "remux.log"));
|
|
114
|
+
expect(xml).toContain(path.join(LOG_DIR, "remux.err"));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("sets WorkingDirectory to package directory", () => {
|
|
118
|
+
const xml = service.generatePlist({});
|
|
119
|
+
expect(xml).toContain("<key>WorkingDirectory</key>");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("installService", () => {
|
|
124
|
+
it("creates log directory", () => {
|
|
125
|
+
service.installService({});
|
|
126
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith(LOG_DIR, {
|
|
127
|
+
recursive: true,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("writes plist file to LaunchAgents", () => {
|
|
132
|
+
service.installService({});
|
|
133
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
134
|
+
PLIST_PATH,
|
|
135
|
+
expect.stringContaining("com.remux.agent"),
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("runs launchctl load", () => {
|
|
140
|
+
service.installService({});
|
|
141
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
142
|
+
expect.stringContaining("launchctl load"),
|
|
143
|
+
expect.any(Object),
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("passes port option through to plist", () => {
|
|
148
|
+
service.installService({ port: 4000 });
|
|
149
|
+
const writtenXml = mockFs.writeFileSync.mock.calls[0][1];
|
|
150
|
+
expect(writtenXml).toContain("<string>4000</string>");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("unloads existing service before installing", () => {
|
|
154
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
155
|
+
service.installService({});
|
|
156
|
+
// Should unload first, then load
|
|
157
|
+
const calls = mockExecSync.mock.calls.map((c) => c[0]);
|
|
158
|
+
const unloadIdx = calls.findIndex((c) => c.includes("launchctl unload"));
|
|
159
|
+
const loadIdx = calls.findIndex((c) => c.includes("launchctl load"));
|
|
160
|
+
expect(unloadIdx).toBeLessThan(loadIdx);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("uninstallService", () => {
|
|
165
|
+
it("runs launchctl unload", () => {
|
|
166
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
167
|
+
service.uninstallService();
|
|
168
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
169
|
+
expect.stringContaining("launchctl unload"),
|
|
170
|
+
expect.any(Object),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("removes plist file", () => {
|
|
175
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
176
|
+
service.uninstallService();
|
|
177
|
+
expect(mockFs.unlinkSync).toHaveBeenCalledWith(PLIST_PATH);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("handles missing plist gracefully", () => {
|
|
181
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
182
|
+
// Should not throw
|
|
183
|
+
expect(() => service.uninstallService()).not.toThrow();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("serviceStatus", () => {
|
|
188
|
+
it("returns not installed when plist missing", () => {
|
|
189
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
190
|
+
const status = service.serviceStatus();
|
|
191
|
+
expect(status.installed).toBe(false);
|
|
192
|
+
expect(status.running).toBe(false);
|
|
193
|
+
expect(status.pid).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("returns installed + running with PID when launchctl finds it", () => {
|
|
197
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
198
|
+
// launchctl list output: PID, last exit status, label
|
|
199
|
+
mockExecSync.mockReturnValue("12345\t0\tcom.remux.agent\n");
|
|
200
|
+
const status = service.serviceStatus();
|
|
201
|
+
expect(status.installed).toBe(true);
|
|
202
|
+
expect(status.running).toBe(true);
|
|
203
|
+
expect(status.pid).toBe(12345);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("returns installed but not running when PID is -", () => {
|
|
207
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
208
|
+
mockExecSync.mockReturnValue("-\t0\tcom.remux.agent\n");
|
|
209
|
+
const status = service.serviceStatus();
|
|
210
|
+
expect(status.installed).toBe(true);
|
|
211
|
+
expect(status.running).toBe(false);
|
|
212
|
+
expect(status.pid).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("returns installed but not running when launchctl throws", () => {
|
|
216
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
217
|
+
mockExecSync.mockImplementation(() => {
|
|
218
|
+
throw new Error("Could not find service");
|
|
219
|
+
});
|
|
220
|
+
const status = service.serviceStatus();
|
|
221
|
+
expect(status.installed).toBe(true);
|
|
222
|
+
expect(status.running).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("handleServiceCommand", () => {
|
|
227
|
+
it("returns false for non-service commands", () => {
|
|
228
|
+
expect(service.handleServiceCommand([])).toBe(false);
|
|
229
|
+
expect(service.handleServiceCommand(["node", "server.js"])).toBe(false);
|
|
230
|
+
expect(service.handleServiceCommand(["node", "server.js", "start"])).toBe(
|
|
231
|
+
false,
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("returns true for service commands", () => {
|
|
236
|
+
expect(
|
|
237
|
+
service.handleServiceCommand(["node", "server.js", "service", "install"]),
|
|
238
|
+
).toBe(true);
|
|
239
|
+
expect(
|
|
240
|
+
service.handleServiceCommand([
|
|
241
|
+
"node",
|
|
242
|
+
"server.js",
|
|
243
|
+
"service",
|
|
244
|
+
"uninstall",
|
|
245
|
+
]),
|
|
246
|
+
).toBe(true);
|
|
247
|
+
expect(
|
|
248
|
+
service.handleServiceCommand(["node", "server.js", "service", "status"]),
|
|
249
|
+
).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("parses --port flag for install", () => {
|
|
253
|
+
service.handleServiceCommand([
|
|
254
|
+
"node",
|
|
255
|
+
"server.js",
|
|
256
|
+
"service",
|
|
257
|
+
"install",
|
|
258
|
+
"--port",
|
|
259
|
+
"3000",
|
|
260
|
+
]);
|
|
261
|
+
// Verify that writeFileSync was called with port in the plist
|
|
262
|
+
const writtenXml = mockFs.writeFileSync.mock.calls[0]?.[1];
|
|
263
|
+
if (writtenXml) {
|
|
264
|
+
expect(writtenXml).toContain("<string>3000</string>");
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SQLite store (src/store.ts).
|
|
3
|
+
* Device CRUD, trust levels, pairing, and session/tab persistence.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
import {
|
|
9
|
+
_resetDbForTest,
|
|
10
|
+
closeDb,
|
|
11
|
+
upsertSession,
|
|
12
|
+
upsertTab,
|
|
13
|
+
loadSessions,
|
|
14
|
+
removeSession,
|
|
15
|
+
removeStaleTab,
|
|
16
|
+
createDevice,
|
|
17
|
+
findDeviceById,
|
|
18
|
+
findDeviceByFingerprint,
|
|
19
|
+
listDevices,
|
|
20
|
+
updateDeviceTrust,
|
|
21
|
+
renameDevice,
|
|
22
|
+
deleteDevice,
|
|
23
|
+
touchDevice,
|
|
24
|
+
hasAnyDevice,
|
|
25
|
+
computeFingerprint,
|
|
26
|
+
createPairCode,
|
|
27
|
+
consumePairCode,
|
|
28
|
+
} from "../src/store.ts";
|
|
29
|
+
|
|
30
|
+
/** Create an in-memory SQLite DB with the same schema as store.ts. */
|
|
31
|
+
function createTestDb() {
|
|
32
|
+
const db = new Database(":memory:");
|
|
33
|
+
db.pragma("journal_mode = WAL");
|
|
34
|
+
db.pragma("foreign_keys = ON");
|
|
35
|
+
db.exec(`
|
|
36
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
37
|
+
name TEXT PRIMARY KEY,
|
|
38
|
+
created_at INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS tabs (
|
|
41
|
+
id INTEGER PRIMARY KEY,
|
|
42
|
+
session_name TEXT NOT NULL,
|
|
43
|
+
title TEXT NOT NULL DEFAULT 'Tab',
|
|
44
|
+
scrollback BLOB,
|
|
45
|
+
ended INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
FOREIGN KEY (session_name) REFERENCES sessions(name) ON DELETE CASCADE
|
|
47
|
+
);
|
|
48
|
+
CREATE TABLE IF NOT EXISTS devices (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
name TEXT NOT NULL,
|
|
51
|
+
fingerprint TEXT NOT NULL,
|
|
52
|
+
trust TEXT NOT NULL DEFAULT 'untrusted',
|
|
53
|
+
created_at INTEGER NOT NULL,
|
|
54
|
+
last_seen INTEGER NOT NULL
|
|
55
|
+
);
|
|
56
|
+
CREATE TABLE IF NOT EXISTS pair_codes (
|
|
57
|
+
code TEXT PRIMARY KEY,
|
|
58
|
+
created_by TEXT NOT NULL,
|
|
59
|
+
expires_at INTEGER NOT NULL,
|
|
60
|
+
FOREIGN KEY (created_by) REFERENCES devices(id) ON DELETE CASCADE
|
|
61
|
+
);
|
|
62
|
+
`);
|
|
63
|
+
return db;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("store: session/tab persistence", () => {
|
|
67
|
+
let db;
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
db = createTestDb();
|
|
71
|
+
_resetDbForTest(db);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
closeDb();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("upserts and loads sessions", () => {
|
|
79
|
+
upsertSession("main", 1000);
|
|
80
|
+
upsertSession("work", 2000);
|
|
81
|
+
|
|
82
|
+
const sessions = loadSessions();
|
|
83
|
+
expect(sessions).toHaveLength(2);
|
|
84
|
+
expect(sessions[0].name).toBe("main");
|
|
85
|
+
expect(sessions[0].createdAt).toBe(1000);
|
|
86
|
+
expect(sessions[1].name).toBe("work");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("upserts and loads tabs with scrollback as BLOB", () => {
|
|
90
|
+
upsertSession("main", 1000);
|
|
91
|
+
const scrollback = Buffer.from("hello world terminal output");
|
|
92
|
+
upsertTab({
|
|
93
|
+
id: 1,
|
|
94
|
+
sessionName: "main",
|
|
95
|
+
title: "Tab 1",
|
|
96
|
+
scrollback,
|
|
97
|
+
ended: false,
|
|
98
|
+
});
|
|
99
|
+
upsertTab({
|
|
100
|
+
id: 2,
|
|
101
|
+
sessionName: "main",
|
|
102
|
+
title: "Tab 2",
|
|
103
|
+
scrollback: null,
|
|
104
|
+
ended: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const sessions = loadSessions();
|
|
108
|
+
expect(sessions[0].tabs).toHaveLength(2);
|
|
109
|
+
expect(Buffer.isBuffer(sessions[0].tabs[0].scrollback)).toBe(true);
|
|
110
|
+
expect(sessions[0].tabs[0].scrollback.toString()).toBe(
|
|
111
|
+
"hello world terminal output",
|
|
112
|
+
);
|
|
113
|
+
expect(sessions[0].tabs[0].ended).toBe(false);
|
|
114
|
+
expect(sessions[0].tabs[1].scrollback).toBeNull();
|
|
115
|
+
expect(sessions[0].tabs[1].ended).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("upsert updates existing tab data", () => {
|
|
119
|
+
upsertSession("main", 1000);
|
|
120
|
+
upsertTab({
|
|
121
|
+
id: 1,
|
|
122
|
+
sessionName: "main",
|
|
123
|
+
title: "Tab 1",
|
|
124
|
+
scrollback: Buffer.from("old"),
|
|
125
|
+
ended: false,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
upsertTab({
|
|
129
|
+
id: 1,
|
|
130
|
+
sessionName: "main",
|
|
131
|
+
title: "Renamed",
|
|
132
|
+
scrollback: Buffer.from("new data"),
|
|
133
|
+
ended: false,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const sessions = loadSessions();
|
|
137
|
+
expect(sessions[0].tabs).toHaveLength(1);
|
|
138
|
+
expect(sessions[0].tabs[0].title).toBe("Renamed");
|
|
139
|
+
expect(sessions[0].tabs[0].scrollback.toString()).toBe("new data");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("removeSession cascades to tabs", () => {
|
|
143
|
+
upsertSession("temp", 3000);
|
|
144
|
+
upsertTab({
|
|
145
|
+
id: 10,
|
|
146
|
+
sessionName: "temp",
|
|
147
|
+
title: "Tab",
|
|
148
|
+
scrollback: null,
|
|
149
|
+
ended: false,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
removeSession("temp");
|
|
153
|
+
const sessions = loadSessions();
|
|
154
|
+
expect(sessions).toHaveLength(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("removeStaleTab deletes individual tabs", () => {
|
|
158
|
+
upsertSession("main", 1000);
|
|
159
|
+
upsertTab({
|
|
160
|
+
id: 1,
|
|
161
|
+
sessionName: "main",
|
|
162
|
+
title: "Tab 1",
|
|
163
|
+
scrollback: null,
|
|
164
|
+
ended: false,
|
|
165
|
+
});
|
|
166
|
+
upsertTab({
|
|
167
|
+
id: 2,
|
|
168
|
+
sessionName: "main",
|
|
169
|
+
title: "Tab 2",
|
|
170
|
+
scrollback: null,
|
|
171
|
+
ended: false,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
removeStaleTab(1);
|
|
175
|
+
const sessions = loadSessions();
|
|
176
|
+
expect(sessions[0].tabs).toHaveLength(1);
|
|
177
|
+
expect(sessions[0].tabs[0].id).toBe(2);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("store: device CRUD", () => {
|
|
182
|
+
let db;
|
|
183
|
+
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
db = createTestDb();
|
|
186
|
+
_resetDbForTest(db);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
afterEach(() => {
|
|
190
|
+
closeDb();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("creates a device with generated ID", () => {
|
|
194
|
+
const device = createDevice("fp123", "untrusted");
|
|
195
|
+
expect(device.id).toHaveLength(16);
|
|
196
|
+
expect(device.fingerprint).toBe("fp123");
|
|
197
|
+
expect(device.trust).toBe("untrusted");
|
|
198
|
+
expect(device.name).toMatch(/^Device-/);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("creates a device with custom name", () => {
|
|
202
|
+
const device = createDevice("fp123", "trusted", "My Phone");
|
|
203
|
+
expect(device.name).toBe("My Phone");
|
|
204
|
+
expect(device.trust).toBe("trusted");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("findDeviceById returns device or null", () => {
|
|
208
|
+
const device = createDevice("fp-find", "untrusted");
|
|
209
|
+
const found = findDeviceById(device.id);
|
|
210
|
+
expect(found).not.toBeNull();
|
|
211
|
+
expect(found.fingerprint).toBe("fp-find");
|
|
212
|
+
|
|
213
|
+
const notFound = findDeviceById("nonexistent");
|
|
214
|
+
expect(notFound).toBeNull();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("findDeviceByFingerprint returns device or null", () => {
|
|
218
|
+
createDevice("fp-search", "untrusted");
|
|
219
|
+
const found = findDeviceByFingerprint("fp-search");
|
|
220
|
+
expect(found).not.toBeNull();
|
|
221
|
+
|
|
222
|
+
const notFound = findDeviceByFingerprint("nonexistent");
|
|
223
|
+
expect(notFound).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("listDevices returns all devices", () => {
|
|
227
|
+
const d1 = createDevice("fp1", "untrusted");
|
|
228
|
+
const d2 = createDevice("fp2", "trusted");
|
|
229
|
+
|
|
230
|
+
const devices = listDevices();
|
|
231
|
+
expect(devices).toHaveLength(2);
|
|
232
|
+
const ids = devices.map((d) => d.id);
|
|
233
|
+
expect(ids).toContain(d1.id);
|
|
234
|
+
expect(ids).toContain(d2.id);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("hasAnyDevice returns correct boolean", () => {
|
|
238
|
+
expect(hasAnyDevice()).toBe(false);
|
|
239
|
+
createDevice("fp-check", "untrusted");
|
|
240
|
+
expect(hasAnyDevice()).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("store: trust levels", () => {
|
|
245
|
+
let db;
|
|
246
|
+
|
|
247
|
+
beforeEach(() => {
|
|
248
|
+
db = createTestDb();
|
|
249
|
+
_resetDbForTest(db);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
afterEach(() => {
|
|
253
|
+
closeDb();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("updateDeviceTrust changes trust level", () => {
|
|
257
|
+
const device = createDevice("fp-trust", "untrusted");
|
|
258
|
+
expect(device.trust).toBe("untrusted");
|
|
259
|
+
|
|
260
|
+
updateDeviceTrust(device.id, "trusted");
|
|
261
|
+
const updated = findDeviceById(device.id);
|
|
262
|
+
expect(updated.trust).toBe("trusted");
|
|
263
|
+
|
|
264
|
+
updateDeviceTrust(device.id, "blocked");
|
|
265
|
+
const blocked = findDeviceById(device.id);
|
|
266
|
+
expect(blocked.trust).toBe("blocked");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("updateDeviceTrust returns false for nonexistent device", () => {
|
|
270
|
+
const result = updateDeviceTrust("nonexistent", "trusted");
|
|
271
|
+
expect(result).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("renameDevice changes the device name", () => {
|
|
275
|
+
const device = createDevice("fp-rename", "untrusted");
|
|
276
|
+
renameDevice(device.id, "New Name");
|
|
277
|
+
const updated = findDeviceById(device.id);
|
|
278
|
+
expect(updated.name).toBe("New Name");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("deleteDevice removes the device", () => {
|
|
282
|
+
const device = createDevice("fp-del", "untrusted");
|
|
283
|
+
expect(deleteDevice(device.id)).toBe(true);
|
|
284
|
+
expect(findDeviceById(device.id)).toBeNull();
|
|
285
|
+
expect(deleteDevice("nonexistent")).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("touchDevice updates last_seen", () => {
|
|
289
|
+
const device = createDevice("fp-touch", "untrusted");
|
|
290
|
+
const before = device.lastSeen;
|
|
291
|
+
|
|
292
|
+
// Small delay to ensure timestamp changes
|
|
293
|
+
const now = Date.now() + 1000;
|
|
294
|
+
const origNow = Date.now;
|
|
295
|
+
|
|
296
|
+
touchDevice(device.id);
|
|
297
|
+
const updated = findDeviceById(device.id);
|
|
298
|
+
expect(updated.lastSeen).toBeGreaterThanOrEqual(before);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe("store: pairing codes", () => {
|
|
303
|
+
let db;
|
|
304
|
+
|
|
305
|
+
beforeEach(() => {
|
|
306
|
+
db = createTestDb();
|
|
307
|
+
_resetDbForTest(db);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
afterEach(() => {
|
|
311
|
+
closeDb();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("creates a valid 6-digit pair code", () => {
|
|
315
|
+
const device = createDevice("fp-pair", "trusted");
|
|
316
|
+
const pc = createPairCode(device.id);
|
|
317
|
+
expect(pc.code).toHaveLength(6);
|
|
318
|
+
expect(Number(pc.code)).toBeGreaterThanOrEqual(100000);
|
|
319
|
+
expect(Number(pc.code)).toBeLessThan(1000000);
|
|
320
|
+
expect(pc.expiresAt).toBeGreaterThan(Date.now());
|
|
321
|
+
expect(pc.createdBy).toBe(device.id);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("consumePairCode returns creator and is one-time use", () => {
|
|
325
|
+
const device = createDevice("fp-pair-consume", "trusted");
|
|
326
|
+
const pc = createPairCode(device.id);
|
|
327
|
+
|
|
328
|
+
const creator = consumePairCode(pc.code);
|
|
329
|
+
expect(creator).toBe(device.id);
|
|
330
|
+
|
|
331
|
+
// Second use should fail (one-time)
|
|
332
|
+
const second = consumePairCode(pc.code);
|
|
333
|
+
expect(second).toBeNull();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("expired code returns null", () => {
|
|
337
|
+
const device = createDevice("fp-pair-expired", "trusted");
|
|
338
|
+
// Manually insert an expired code
|
|
339
|
+
db.prepare(
|
|
340
|
+
"INSERT INTO pair_codes (code, created_by, expires_at) VALUES (?, ?, ?)",
|
|
341
|
+
).run("999999", device.id, Date.now() - 1000);
|
|
342
|
+
|
|
343
|
+
const result = consumePairCode("999999");
|
|
344
|
+
expect(result).toBeNull();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("invalid code returns null", () => {
|
|
348
|
+
const result = consumePairCode("000000");
|
|
349
|
+
expect(result).toBeNull();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("store: computeFingerprint", () => {
|
|
354
|
+
it("produces consistent 16-char hex fingerprints", () => {
|
|
355
|
+
const fp = computeFingerprint("Mozilla/5.0", "en-US");
|
|
356
|
+
expect(fp).toHaveLength(16);
|
|
357
|
+
expect(fp).toMatch(/^[a-f0-9]{16}$/);
|
|
358
|
+
|
|
359
|
+
// Same input => same output
|
|
360
|
+
const fp2 = computeFingerprint("Mozilla/5.0", "en-US");
|
|
361
|
+
expect(fp2).toBe(fp);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("different inputs produce different fingerprints", () => {
|
|
365
|
+
const fp1 = computeFingerprint("Chrome/120", "en-US");
|
|
366
|
+
const fp2 = computeFingerprint("Safari/17", "en-US");
|
|
367
|
+
expect(fp1).not.toBe(fp2);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for tunnel support (CLI arg parsing + URL building).
|
|
3
|
+
* Does NOT spawn cloudflared — pure logic tests only.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from "vitest";
|
|
7
|
+
import { parseTunnelArgs, buildTunnelAccessUrl } from "../src/tunnel.js";
|
|
8
|
+
|
|
9
|
+
// ── parseTunnelArgs ─────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
describe("parseTunnelArgs", () => {
|
|
12
|
+
it("returns auto when no tunnel flags present", () => {
|
|
13
|
+
const result = parseTunnelArgs(["node", "server.js"]);
|
|
14
|
+
expect(result.tunnelMode).toBe("auto");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns enable when --tunnel is passed", () => {
|
|
18
|
+
const result = parseTunnelArgs(["node", "server.js", "--tunnel"]);
|
|
19
|
+
expect(result.tunnelMode).toBe("enable");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns disable when --no-tunnel is passed", () => {
|
|
23
|
+
const result = parseTunnelArgs(["node", "server.js", "--no-tunnel"]);
|
|
24
|
+
expect(result.tunnelMode).toBe("disable");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("--no-tunnel takes precedence over --tunnel", () => {
|
|
28
|
+
const result = parseTunnelArgs(["node", "server.js", "--tunnel", "--no-tunnel"]);
|
|
29
|
+
expect(result.tunnelMode).toBe("disable");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("works with other flags mixed in", () => {
|
|
33
|
+
const result = parseTunnelArgs(["node", "server.js", "--password", "secret", "--tunnel"]);
|
|
34
|
+
expect(result.tunnelMode).toBe("enable");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("ignores unrelated flags", () => {
|
|
38
|
+
const result = parseTunnelArgs(["node", "server.js", "--password", "secret", "--port", "9000"]);
|
|
39
|
+
expect(result.tunnelMode).toBe("auto");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── buildTunnelAccessUrl ────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe("buildTunnelAccessUrl", () => {
|
|
46
|
+
const BASE = "https://abc-xyz.trycloudflare.com";
|
|
47
|
+
|
|
48
|
+
it("appends token when token auth is used", () => {
|
|
49
|
+
const url = buildTunnelAccessUrl(BASE, "mytoken123", null);
|
|
50
|
+
expect(url).toBe(`${BASE}?token=mytoken123`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns plain URL when password auth is used (no token)", () => {
|
|
54
|
+
const url = buildTunnelAccessUrl(BASE, null, "mypassword");
|
|
55
|
+
expect(url).toBe(BASE);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns plain URL when no auth", () => {
|
|
59
|
+
const url = buildTunnelAccessUrl(BASE, null, null);
|
|
60
|
+
expect(url).toBe(BASE);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("appends token even when password is also set (token takes priority)", () => {
|
|
64
|
+
const url = buildTunnelAccessUrl(BASE, "tok", "pw");
|
|
65
|
+
expect(url).toBe(`${BASE}?token=tok`);
|
|
66
|
+
});
|
|
67
|
+
});
|