Skip to content

Commit 28d2701

Browse files
ZhaoJiSenfeng626
authored andcommitted
test(ui): cover useChenSession and connectionProgressOverlay and useWork
Commit 9 file(s) from the captured immutable snapshot. Notable diff evidence: - expect(markDisconnected).toHaveBeenCalledWith(expect.any(String), { dismissible: true }); - describe("workspace connection progress overlay", () => { - it("allows post-connection failures to reveal the underlying connector output", () => {
1 parent 76b663f commit 28d2701

9 files changed

Lines changed: 134 additions & 15 deletions

File tree

‎ui/chen/composables/useChenSession.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ describe("chen session dialogs", () => {
9292
await Promise.resolve();
9393
socket.onclose?.({ code: 1006, reason: "" } as CloseEvent);
9494

95-
expect(markDisconnected).toHaveBeenCalled();
95+
expect(markDisconnected).toHaveBeenCalledWith(expect.any(String), { dismissible: true });
9696
});
9797

9898
it("marks a session as disconnected when its socket closes before ready", async () => {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, expect, it } from "vitest";
2+
import overlay from "./connectionProgressOverlay.vue?raw";
3+
4+
describe("workspace connection progress overlay", () => {
5+
it("allows post-connection failures to reveal the underlying connector output", () => {
6+
expect(overlay).toContain("dismissible?: boolean");
7+
expect(overlay).toContain('v-show="!dismissed"');
8+
expect(overlay).toContain('v-if="failed && dismissible"');
9+
expect(overlay).toContain('class="absolute top-6 right-6 z-10"');
10+
expect(overlay).toContain('@click="dismiss"');
11+
});
12+
13+
it("preserves normalized diagnostic line breaks", () => {
14+
expect(overlay).toContain("whitespace-pre-wrap");
15+
expect(overlay).toContain("wrap-break-word");
16+
});
17+
});

‎ui/composables/useWorkspaceTabs.sessionStatus.test.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ describe("workspace session disconnect status", () => {
4545
expect(tabs.tabs.value[0]?.status).toBe("disconnected");
4646
});
4747

48-
it("keeps a ready session and its disconnect reason", () => {
48+
it("keeps a ready session and a safe disconnect reason", () => {
4949
const pane = tabs.openSession(asset, { protocol: "ssh", account: "root", newTab: true });
5050
pane.payload = { id: "token" };
5151
tabs.markSessionConnected(pane.id);
5252

53-
tabs.markSessionDisconnected(pane.id, "connection closed");
53+
tabs.markSessionDisconnected(pane.id, "\x1B[31mconnection closed\x1B[0m");
5454

5555
expect(pane.status).toBe("disconnected");
5656
expect(pane.payload).toEqual({ id: "token" });
@@ -66,11 +66,16 @@ describe("workspace session disconnect status", () => {
6666
const pane = tabs.openSetupSession(asset);
6767
tabs.startSessionConnection(pane.id, { protocol: "ssh", account: "root" });
6868
tabs.markSessionConnected(pane.id);
69-
tabs.markSessionDisconnected(pane.id, "Koko 已结束会话:连接建立失败");
69+
tabs.markSessionDisconnected(pane.id, "Koko 已结束会话:连接建立失败", { dismissible: true });
7070

7171
expect(pane.status).toBe("disconnected");
7272
expect(pane.connectionProgress).toBe("connected");
7373
expect(pane.connectionFailure).toBe("Koko 已结束会话:连接建立失败");
74+
expect(pane.connectionFailureDismissible).toBe(true);
75+
expect(tabs.tabs.value[0]?.connectionFailureDismissible).toBe(true);
76+
77+
tabs.markSessionConnecting(pane.id);
78+
expect(pane.connectionFailureDismissible).toBeUndefined();
7479
});
7580

7681
it("increments the connection attempt before a new request can update the pane", () => {

‎ui/koko/tests/kubernetes/kubernetesWorkspace.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,10 @@ describe("kubernetes workspace click typing", () => {
1919
expect(kubernetesWorkspace).not.toMatch(/\.xterm-scrollable-element\s*\{[^}]*padding:/);
2020
expect(kubernetesWorkspace).not.toContain("background-color: transparent !important");
2121
});
22+
23+
it("marks post-connection protocol and transport failures as dismissible", () => {
24+
expect(kubernetesWorkspace).toContain("const connectorConnected = ref(false)");
25+
expect(kubernetesWorkspace).toContain("{ dismissible: connectorConnected.value }");
26+
expect(kubernetesWorkspace).toContain("const stopFailureListener = terminalSocket.onFailure((failure) =>");
27+
});
2228
});

‎ui/koko/tests/sftp/sftp.test.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { FileTransferStatus, FileTransferTask } from "@jumpserver/connectors-core";
22
import { afterEach, describe, expect, it, vi } from "vitest";
3-
import { ref } from "vue";
3+
import { effectScope, ref } from "vue";
44

55
import { encodeSftpBinaryFrame, parseSftpBinaryFrame } from "#koko/composables/sftp/core/codec";
66
import {
@@ -19,6 +19,7 @@ import {
1919
SftpSocketFailureCode,
2020
SftpWebSocketProtocol
2121
} from "#koko/composables/sftp/protocol";
22+
import { useSftpFileManager } from "#koko/composables/sftp/useSftpFileManager";
2223
import {
2324
SFTP_UPLOAD_CHUNK_SIZE,
2425
SftpPathNotFoundError,
@@ -33,6 +34,10 @@ import { finishedTransferCount, sftpTransferGroupStatus, sftpTransferProgress }
3334
import enMessages from "../../../../i18n/locales/en.json";
3435
import zhMessages from "../../../../i18n/locales/zh.json";
3536

37+
vi.mock("#koko/host", () => ({
38+
useKokoHostAdapter: () => ({})
39+
}));
40+
3641
const context = {
3742
component: "koko" as const,
3843
tokenId: "initial-token",
@@ -174,6 +179,22 @@ describe("sFTP browser protocol", () => {
174179
expect(socket.connected.value).toBe(false);
175180
});
176181

182+
it("uses a server SFTP error as the fatal session reason", () => {
183+
vi.stubGlobal("WebSocket", FakeWebSocket as unknown as typeof WebSocket);
184+
vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
185+
const scope = effectScope();
186+
const manager = scope.run(() => useSftpFileManager(ref(context)))!;
187+
const fake = FakeWebSocket.instances.at(-1)!;
188+
189+
fake.open();
190+
fake.receive({ id: "error", type: SftpMessageType.Error, err: "Permission denied" });
191+
192+
expect(manager.fatalError.value).toBe(true);
193+
expect(manager.hasConnected.value).toBe(true);
194+
expect(manager.error.value).toBe("koko.fileManagement.pathPermissionDenied");
195+
scope.stop();
196+
});
197+
177198
it("disconnects when koko stops sending heartbeats", () => {
178199
vi.useFakeTimers();
179200
const { fake, socket } = openSocket();

‎ui/koko/tests/sftp/sftpTransportCore.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ function createSocket(connected = true) {
2727
} = {
2828
socket: ref(null),
2929
connected: ref(connected),
30+
hasConnected: ref(connected),
3031
failure: ref(null),
3132
close: vi.fn(),
3233
connect: vi.fn(),

‎ui/koko/tests/terminal/terminalConnection.test.ts‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
} from "#koko/composables/terminal/protocol";
99
import { useKokoTerminalHeartbeat } from "#koko/composables/terminal/useTerminalHeartbeat";
1010
import { useKokoTerminalTransport } from "#koko/composables/terminal/useTerminalTransport";
11+
import { normalizeConnectionFailure } from "~/utils/connectionFailure";
12+
import terminalSocketSource from "../../composables/terminal/useTerminalSocket.ts?raw";
1113

1214
afterEach(() => {
1315
vi.useRealTimers();
@@ -30,21 +32,28 @@ it("distinguishes Koko session end, Koko timeout, offline and unexplained closes
3032
expect(describeTerminalClose(abnormal, "", true).source).toBe("koko");
3133
});
3234

33-
it("uses raw startup output for an early close and clears it once ready", () => {
35+
it("uses sanitized startup bytes for an early close and clears them once ready", () => {
3436
const capture = createKokoStartupOutputCapture();
35-
const encoded = new TextEncoder().encode("sqlserver: 连接被拒绝");
37+
const fallback = "Koko 已结束会话:资产连接已结束";
38+
const encoded = new TextEncoder().encode("\u001b[31m认证失败\u001b[0m");
39+
const prefixBytes = new TextEncoder().encode("\u001b[31m").length;
40+
const splitAt = prefixBytes + 1;
3641

37-
capture.append(encoded.slice(0, 13));
38-
capture.append(encoded.slice(13));
39-
expect(resolveKokoTerminalCloseMessage(capture.take(), "Koko 已结束会话:资产连接已结束")).toBe(
40-
"sqlserver: 连接被拒绝"
42+
expect(new TextEncoder().encode("认").length).toBeGreaterThan(1);
43+
capture.append(encoded.slice(0, splitAt));
44+
capture.append(encoded.slice(splitAt));
45+
const startup = normalizeConnectionFailure(capture.take());
46+
47+
expect(startup).toBe("认证失败");
48+
expect(normalizeConnectionFailure("开始连接\r\nssh: handshake failed: EOF")).toBe(
49+
"开始连接\nssh: handshake failed: EOF"
4150
);
51+
expect(startup).not.toContain("\u001b");
52+
expect(resolveKokoTerminalCloseMessage(startup, fallback)).toBe("认证失败");
4253

4354
capture.markReady();
4455
capture.append(new TextEncoder().encode("must not replace the close reason"));
45-
expect(resolveKokoTerminalCloseMessage(capture.take(), "Koko 已结束会话:资产连接已结束")).toBe(
46-
"Koko 已结束会话:资产连接已结束"
47-
);
56+
expect(resolveKokoTerminalCloseMessage(normalizeConnectionFailure(capture.take()), fallback)).toBe(fallback);
4857
});
4958

5059
it("keeps only the last 8 KiB of startup output", () => {
@@ -58,6 +67,14 @@ it("keeps only the last 8 KiB of startup output", () => {
5867
expect(capture.take()).toBe(`${"x".repeat(limit - 4)}tail`);
5968
});
6069

70+
it("reads sanitized startup bytes on close instead of the xterm buffer", () => {
71+
expect(terminalSocketSource).toContain("normalizeConnectionFailure(startupOutput.take())");
72+
expect(terminalSocketSource).toContain("socketOpened = true");
73+
expect(terminalSocketSource).toContain("{ dismissible: socketOpened }");
74+
expect(terminalSocketSource).toContain("onTerminalReady: startupOutput.markReady");
75+
expect(terminalSocketSource).not.toContain("getXTerminalLineContent");
76+
});
77+
6178
it("sends heartbeats only on open sockets, updates send time and stops after close", () => {
6279
vi.useFakeTimers();
6380
const socket = { readyState: WebSocket.CONNECTING as number, send: vi.fn() };

‎ui/lion/workspaces/useBaseWorkspaceSession.test.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { WorkspaceSessionTab } from "~/composables/useWorkspaceTabs";
2-
import { effectScope, ref } from "vue";
32
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import { effectScope, ref } from "vue";
44

55
const markSessionConnected = vi.fn();
66
const markSessionFailed = vi.fn();
@@ -20,6 +20,26 @@ describe("lion workspace session preparation", () => {
2020
createLionConnectTicket.mockResolvedValue("ticket-1");
2121
});
2222

23+
it("fails the workspace when its connection token is missing", async () => {
24+
const tab = ref({
25+
id: "tab-1",
26+
assetId: "asset-1",
27+
protocol: "rdp",
28+
account: "root",
29+
payload: {}
30+
} as WorkspaceSessionTab);
31+
const scope = effectScope();
32+
const session = scope.run(() => useBaseWorkspaceSession(tab))!;
33+
34+
await session.prepareSession();
35+
36+
expect(markSessionFailed).toHaveBeenCalledWith(
37+
{ tabId: "tab-1", assetId: "asset-1", protocol: "rdp", account: "root" },
38+
"Missing connection token"
39+
);
40+
scope.stop();
41+
});
42+
2343
it("waits for Guacamole readiness before marking the session connected", async () => {
2444
const tab = ref({
2545
id: "tab-1",

‎ui/utils/connectionFailure.test.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from "vitest";
2+
import { normalizeConnectionFailure } from "~/utils/connectionFailure";
3+
4+
describe("normalizeConnectionFailure", () => {
5+
it("removes terminal control sequences and HTML while preserving readable lines", () => {
6+
expect(
7+
normalizeConnectionFailure("\u001B[31m<p>Connecting</p>\r\n<p>Authentication failed\u0000</p>\u001B[0m")
8+
).toBe("Connecting\nAuthentication failed");
9+
});
10+
11+
it("drops a binary prefix while retaining the terminal diagnostic", () => {
12+
expect(normalizeConnectionFailure("\u0000\u0000\u0000\u00102.3.4开始连接 root: ssh: handshake failed: EOF")).toBe(
13+
"开始连接 root: ssh: handshake failed: EOF"
14+
);
15+
});
16+
17+
it("removes binary packets embedded between terminal lines", () => {
18+
expect(
19+
normalizeConnectionFailure(
20+
"开始连接 root(root)\u0000.\u0001.\u0002.\u0003.\u0004.\u0005.\u0006.\u0007.\u0008.\u0009开始连接 root(root) error: ssh: handshake failed: EOF"
21+
)
22+
).toBe("开始连接 root(root)\n开始连接 root(root) error: ssh: handshake failed: EOF");
23+
});
24+
25+
it("keeps the last ten non-empty lines", () => {
26+
const message = Array.from({ length: 12 }, (_, index) => `line-${index + 1}`).join("\n");
27+
28+
expect(normalizeConnectionFailure(message)).toBe(
29+
Array.from({ length: 10 }, (_, index) => `line-${index + 3}`).join("\n")
30+
);
31+
});
32+
});

0 commit comments

Comments
 (0)