webssh2/tests/ssh.test.js
2024-11-29 22:28:56 +00:00

291 lines
8 KiB
JavaScript

/* eslint-disable jest/no-conditional-expect */
// server
// tests/ssh.test.js
const SSH2 = require("ssh2")
const SSHConnection = require("../app/ssh")
const { SSHConnectionError } = require("../app/errors")
const { DEFAULTS } = require("../app/constants")
jest.mock("ssh2")
jest.mock("../app/logger", () => ({
createNamespacedDebug: jest.fn(() => jest.fn()),
logError: jest.fn()
}))
jest.mock("../app/utils", () => ({
maskSensitiveData: jest.fn((data) => data)
}))
jest.mock("../app/errors", () => ({
SSHConnectionError: jest.fn(function (message) {
this.message = message
}),
handleError: jest.fn()
}))
describe("SSHConnection", () => {
let sshConnection
let mockConfig
let mockSSH2Client
let registeredEventHandlers
beforeEach(() => {
registeredEventHandlers = {}
mockConfig = {
ssh: {
algorithms: {
kex: ["algo1", "algo2"],
cipher: ["cipher1", "cipher2"],
serverHostKey: ["ssh-rsa", "ssh-dss"],
hmac: ["hmac1", "hmac2"],
compress: ["none", "zlib"]
},
readyTimeout: 20000,
keepaliveInterval: 60000,
keepaliveCountMax: 10
},
user: {
name: null,
password: null,
privatekey: null
}
}
mockSSH2Client = {
on: jest.fn((event, handler) => {
registeredEventHandlers[event] = handler
}),
connect: jest.fn(() => {
process.nextTick(() => {
// By default, emit ready event unless test modifies this behavior
if (registeredEventHandlers.ready) {
registeredEventHandlers.ready()
}
})
}),
shell: jest.fn(),
end: jest.fn()
}
SSH2.Client.mockImplementation(() => mockSSH2Client)
sshConnection = new SSHConnection(mockConfig)
})
afterEach(() => {
jest.clearAllMocks()
})
describe("connect", () => {
it("should handle immediate connection errors", () => {
const mockCreds = {
host: "localhost",
port: 22,
username: "user",
password: "pass"
}
// Mock the connect method to throw an error immediately
mockSSH2Client.connect.mockImplementation(() => {
throw new Error("Spooky Error") // Immediate error
})
return sshConnection.connect(mockCreds).catch((error) => {
expect(error).toBeInstanceOf(SSHConnectionError)
expect(error.message).toBe("Connection failed: Spooky Error")
})
})
it("should connect successfully with password", () => {
const mockCreds = {
host: "localhost",
port: 22,
username: "user",
password: "pass"
}
return sshConnection.connect(mockCreds).then(() => {
expect(mockSSH2Client.connect).toHaveBeenCalledWith(
expect.objectContaining({
host: mockCreds.host,
port: mockCreds.port,
username: mockCreds.username,
password: mockCreds.password,
tryKeyboard: true
})
)
})
})
it("should fail after max authentication attempts", () => {
const mockCreds = {
host: "localhost",
port: 22,
username: "user",
password: "wrongpass"
}
const attempts = DEFAULTS.MAX_AUTH_ATTEMPTS + 1
mockSSH2Client.connect.mockImplementation(() => {
process.nextTick(() => {
registeredEventHandlers.error(new Error("Authentication failed"))
})
})
return sshConnection.connect(mockCreds).catch((error) => {
expect(error).toBeInstanceOf(SSHConnectionError)
expect(error.message).toBe("All authentication methods failed")
expect(mockSSH2Client.connect.mock.calls.length).toBe(attempts)
})
})
describe("key authentication", () => {
const validPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpTestKeyContentHere
-----END RSA PRIVATE KEY-----`
it("should try private key first when both password and key are provided", () => {
const mockCreds = {
host: "localhost",
port: 22,
username: "user",
password: "pass",
privatekey: validPrivateKey
}
return sshConnection.connect(mockCreds).then(() => {
expect(mockSSH2Client.connect).toHaveBeenCalledWith(
expect.objectContaining({
privateKey: validPrivateKey,
username: mockCreds.username
})
)
})
})
it("should fall back to password after key authentication failure", () => {
const mockCreds = {
host: "localhost",
port: 22,
username: "user",
password: "pass",
privatekey: validPrivateKey
}
let authAttempts = 0
mockSSH2Client.connect
.mockImplementationOnce(() => {
process.nextTick(() => {
authAttempts += 1
registeredEventHandlers.error(
new Error("Key authentication failed")
)
})
})
.mockImplementationOnce(() => {
process.nextTick(() => {
authAttempts += 1
registeredEventHandlers.ready()
})
})
return sshConnection.connect(mockCreds).then(() => {
expect(authAttempts).toBe(2)
expect(mockSSH2Client.connect).toHaveBeenCalledTimes(2)
// Verify second attempt used password
expect(mockSSH2Client.connect).toHaveBeenLastCalledWith(
expect.objectContaining({
password: mockCreds.password
})
)
})
})
it("should reject invalid private key format", () => {
const mockCreds = {
host: "localhost",
port: 22,
username: "user",
privatekey: "invalid-key-format"
}
return sshConnection.connect(mockCreds).catch((error) => {
expect(error).toBeInstanceOf(SSHConnectionError)
expect(error.message).toBe("Invalid private key format")
})
})
})
})
describe("shell", () => {
beforeEach(() => {
sshConnection.conn = mockSSH2Client
})
it("should open shell successfully", () => {
const mockStream = {
on: jest.fn(),
stderr: { on: jest.fn() }
}
mockSSH2Client.shell.mockImplementation((options, callback) => {
process.nextTick(() => callback(null, mockStream))
})
return sshConnection.shell().then((result) => {
expect(result).toBe(mockStream)
expect(sshConnection.stream).toBe(mockStream)
})
})
it("should handle shell creation errors", () => {
mockSSH2Client.shell.mockImplementation((options, callback) => {
process.nextTick(() => callback(new Error("Shell error")))
})
return sshConnection.shell().catch((error) => {
expect(error.message).toBe("Shell error")
})
})
})
describe("resizeTerminal", () => {
it("should resize terminal if stream exists", () => {
const mockStream = {
setWindow: jest.fn()
}
sshConnection.stream = mockStream
sshConnection.resizeTerminal(80, 24)
expect(mockStream.setWindow).toHaveBeenCalledWith(80, 24)
})
it("should do nothing if stream does not exist", () => {
sshConnection.stream = null
expect(() => sshConnection.resizeTerminal(80, 24)).not.toThrow()
})
})
describe("end", () => {
it("should close stream and connection", () => {
const mockStream = {
end: jest.fn()
}
sshConnection.stream = mockStream
sshConnection.conn = mockSSH2Client
sshConnection.end()
expect(mockStream.end).toHaveBeenCalled()
expect(mockSSH2Client.end).toHaveBeenCalled()
expect(sshConnection.stream).toBeNull()
expect(sshConnection.conn).toBeNull()
})
it("should handle cleanup when no stream or connection exists", () => {
sshConnection.stream = null
sshConnection.conn = null
expect(() => sshConnection.end()).not.toThrow()
})
})
})