chore: finish up feat #379
This commit is contained in:
parent
44ba13cf67
commit
8fa1631196
6 changed files with 180 additions and 88 deletions
|
@ -28,25 +28,36 @@ const { HTTP } = require("./constants")
|
||||||
* If the authentication fails, the function will send a 401 Unauthorized response
|
* If the authentication fails, the function will send a 401 Unauthorized response
|
||||||
* with the appropriate WWW-Authenticate header.
|
* with the appropriate WWW-Authenticate header.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
function createAuthMiddleware(config) {
|
function createAuthMiddleware(config) {
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
return (req, res, next) => {
|
return (req, res, next) => {
|
||||||
if (config.user.name && config.user.password) {
|
// Check if username and either password or private key is configured
|
||||||
|
if (config.user.name && (config.user.password || config.user.privatekey)) {
|
||||||
req.session.sshCredentials = {
|
req.session.sshCredentials = {
|
||||||
username: config.user.name,
|
username: config.user.name
|
||||||
password: config.user.password
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add credentials based on what's available
|
||||||
|
if (config.user.privatekey) {
|
||||||
|
req.session.sshCredentials.privatekey = config.user.privatekey
|
||||||
|
}
|
||||||
|
if (config.user.password) {
|
||||||
|
req.session.sshCredentials.password = config.user.password
|
||||||
|
}
|
||||||
|
|
||||||
req.session.usedBasicAuth = true
|
req.session.usedBasicAuth = true
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
// Scenario 2: Basic Auth
|
// Scenario 2: Basic Auth
|
||||||
|
|
||||||
|
// If no configured credentials, fall back to Basic Auth
|
||||||
debug("auth: Basic Auth")
|
debug("auth: Basic Auth")
|
||||||
const credentials = basicAuth(req)
|
const credentials = basicAuth(req)
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
res.setHeader(HTTP.AUTHENTICATE, HTTP.REALM)
|
res.setHeader(HTTP.AUTHENTICATE, HTTP.REALM)
|
||||||
return res.status(HTTP.UNAUTHORIZED).send(HTTP.AUTH_REQUIRED)
|
return res.status(HTTP.UNAUTHORIZED).send(HTTP.AUTH_REQUIRED)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and sanitize credentials
|
// Validate and sanitize credentials
|
||||||
req.session.sshCredentials = {
|
req.session.sshCredentials = {
|
||||||
username: validator.escape(credentials.name),
|
username: validator.escape(credentials.name),
|
||||||
|
|
|
@ -117,12 +117,17 @@ class WebSSH2Socket extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeConnection(creds) {
|
initializeConnection(creds) {
|
||||||
// const self = this
|
|
||||||
debug(
|
debug(
|
||||||
`initializeConnection: ${this.socket.id}, INITIALIZING SSH CONNECTION: Host: ${creds.host}, creds: %O`,
|
`initializeConnection: ${this.socket.id}, INITIALIZING SSH CONNECTION: Host: ${creds.host}, creds: %O`,
|
||||||
maskSensitiveData(creds)
|
maskSensitiveData(creds)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Add private key from config if available
|
||||||
|
if (this.config.user.privatekey && !creds.privatekey) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
creds.privatekey = this.config.user.privatekey
|
||||||
|
}
|
||||||
|
|
||||||
this.ssh
|
this.ssh
|
||||||
.connect(creds)
|
.connect(creds)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -130,6 +135,7 @@ class WebSSH2Socket extends EventEmitter {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
username: creds.username,
|
username: creds.username,
|
||||||
password: creds.password,
|
password: creds.password,
|
||||||
|
privatekey: creds.privatekey,
|
||||||
host: creds.host,
|
host: creds.host,
|
||||||
port: creds.port
|
port: creds.port
|
||||||
})
|
})
|
||||||
|
|
11
app/ssh.js
11
app/ssh.js
|
@ -93,7 +93,7 @@ class SSHConnection extends EventEmitter {
|
||||||
|
|
||||||
// Check if this is an authentication error and we haven't exceeded max attempts
|
// Check if this is an authentication error and we haven't exceeded max attempts
|
||||||
if (this.authAttempts < DEFAULTS.MAX_AUTH_ATTEMPTS) {
|
if (this.authAttempts < DEFAULTS.MAX_AUTH_ATTEMPTS) {
|
||||||
this.authAttempts++
|
this.authAttempts += 1
|
||||||
debug(
|
debug(
|
||||||
`Authentication attempt ${this.authAttempts} failed, trying password authentication`
|
`Authentication attempt ${this.authAttempts} failed, trying password authentication`
|
||||||
)
|
)
|
||||||
|
@ -172,13 +172,14 @@ class SSHConnection extends EventEmitter {
|
||||||
debug: createNamespacedDebug("ssh2")
|
debug: createNamespacedDebug("ssh2")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If useKey is true and we have a private key, use it
|
// Try private key first if available and useKey is true
|
||||||
if (useKey && this.config.user.privatekey) {
|
if (useKey && (creds.privatekey || this.config.user.privatekey)) {
|
||||||
debug("Using private key authentication")
|
debug("Using private key authentication")
|
||||||
if (!this.validatePrivateKey(this.config.user.privatekey)) {
|
const privateKey = creds.privatekey || this.config.user.privatekey
|
||||||
|
if (!this.validatePrivateKey(privateKey)) {
|
||||||
throw new SSHConnectionError("Invalid private key format")
|
throw new SSHConnectionError("Invalid private key format")
|
||||||
}
|
}
|
||||||
config.privateKey = this.config.user.privatekey
|
config.privateKey = privateKey
|
||||||
} else if (creds.password) {
|
} else if (creds.password) {
|
||||||
debug("Using password authentication")
|
debug("Using password authentication")
|
||||||
config.password = creds.password
|
config.password = creds.password
|
||||||
|
|
23
app/utils.js
23
app/utils.js
|
@ -81,22 +81,39 @@ function getValidatedPort(portInput) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the provided credentials object is valid.
|
* Checks if the provided credentials object is valid.
|
||||||
|
* Valid credentials must have:
|
||||||
|
* - username (string)
|
||||||
|
* - host (string)
|
||||||
|
* - port (number)
|
||||||
|
* AND either:
|
||||||
|
* - password (string) OR
|
||||||
|
* - privatekey (string)
|
||||||
*
|
*
|
||||||
* @param {Object} creds - The credentials object.
|
* @param {Object} creds - The credentials object.
|
||||||
* @param {string} creds.username - The username.
|
* @param {string} creds.username - The username.
|
||||||
* @param {string} creds.password - The password.
|
* @param {string} [creds.password] - The password.
|
||||||
|
* @param {string} [creds.privatekey] - The private key.
|
||||||
* @param {string} creds.host - The host.
|
* @param {string} creds.host - The host.
|
||||||
* @param {number} creds.port - The port.
|
* @param {number} creds.port - The port.
|
||||||
* @returns {boolean} - Returns true if the credentials are valid, otherwise false.
|
* @returns {boolean} - Returns true if the credentials are valid, otherwise false.
|
||||||
*/
|
*/
|
||||||
function isValidCredentials(creds) {
|
function isValidCredentials(creds) {
|
||||||
return !!(
|
const hasRequiredFields = !!(
|
||||||
creds &&
|
creds &&
|
||||||
typeof creds.username === "string" &&
|
typeof creds.username === "string" &&
|
||||||
typeof creds.password === "string" &&
|
|
||||||
typeof creds.host === "string" &&
|
typeof creds.host === "string" &&
|
||||||
typeof creds.port === "number"
|
typeof creds.port === "number"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!hasRequiredFields) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have either password or privatekey
|
||||||
|
const hasPassword = typeof creds.password === "string"
|
||||||
|
const hasPrivateKey = typeof creds.privatekey === "string"
|
||||||
|
|
||||||
|
return hasPassword || hasPrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"lint": "eslint app",
|
"lint": "eslint app",
|
||||||
"lint:fix": "eslint app --fix",
|
"lint:fix": "eslint app --fix",
|
||||||
"watch": "NODE_ENV=development DEBUG=webssh* nodemon index.js -w app/ -w index.js -w config.json -w package.json",
|
"watch": "NODE_ENV=development DEBUG=webssh*,-webssh2:ssh2 nodemon index.js -w app/ -w index.js -w config.json -w package.json",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"release": "standard-version -a -s --release-as patch --commit-all",
|
"release": "standard-version -a -s --release-as patch --commit-all",
|
||||||
"release:dry-run": "standard-version -a -s --release-as patch --dry-run",
|
"release:dry-run": "standard-version -a -s --release-as patch --dry-run",
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
/* eslint-disable jest/no-conditional-expect */
|
||||||
// server
|
// server
|
||||||
// tests/ssh.test.js
|
// tests/ssh.test.js
|
||||||
|
|
||||||
const SSH2 = require("ssh2")
|
const SSH2 = require("ssh2")
|
||||||
const SSHConnection = require("../app/ssh")
|
const SSHConnection = require("../app/ssh")
|
||||||
const { SSHConnectionError } = require("../app/errors")
|
const { SSHConnectionError } = require("../app/errors")
|
||||||
const { maskSensitiveData } = require("../app/utils")
|
const { DEFAULTS } = require("../app/constants")
|
||||||
|
|
||||||
jest.mock("ssh2")
|
jest.mock("ssh2")
|
||||||
jest.mock("../app/logger", () => ({
|
jest.mock("../app/logger", () => ({
|
||||||
|
@ -12,10 +13,10 @@ jest.mock("../app/logger", () => ({
|
||||||
logError: jest.fn()
|
logError: jest.fn()
|
||||||
}))
|
}))
|
||||||
jest.mock("../app/utils", () => ({
|
jest.mock("../app/utils", () => ({
|
||||||
maskSensitiveData: jest.fn(data => data)
|
maskSensitiveData: jest.fn((data) => data)
|
||||||
}))
|
}))
|
||||||
jest.mock("../app/errors", () => ({
|
jest.mock("../app/errors", () => ({
|
||||||
SSHConnectionError: jest.fn(function(message) {
|
SSHConnectionError: jest.fn(function (message) {
|
||||||
this.message = message
|
this.message = message
|
||||||
}),
|
}),
|
||||||
handleError: jest.fn()
|
handleError: jest.fn()
|
||||||
|
@ -25,8 +26,11 @@ describe("SSHConnection", () => {
|
||||||
let sshConnection
|
let sshConnection
|
||||||
let mockConfig
|
let mockConfig
|
||||||
let mockSSH2Client
|
let mockSSH2Client
|
||||||
|
let registeredEventHandlers
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
registeredEventHandlers = {}
|
||||||
|
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
ssh: {
|
ssh: {
|
||||||
algorithms: {
|
algorithms: {
|
||||||
|
@ -46,14 +50,25 @@ describe("SSHConnection", () => {
|
||||||
privatekey: null
|
privatekey: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sshConnection = new SSHConnection(mockConfig)
|
|
||||||
mockSSH2Client = {
|
mockSSH2Client = {
|
||||||
on: jest.fn(),
|
on: jest.fn((event, handler) => {
|
||||||
connect: jest.fn(),
|
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(),
|
shell: jest.fn(),
|
||||||
end: jest.fn()
|
end: jest.fn()
|
||||||
}
|
}
|
||||||
|
|
||||||
SSH2.Client.mockImplementation(() => mockSSH2Client)
|
SSH2.Client.mockImplementation(() => mockSSH2Client)
|
||||||
|
sshConnection = new SSHConnection(mockConfig)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -61,96 +76,142 @@ describe("SSHConnection", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("connect", () => {
|
describe("connect", () => {
|
||||||
it("should handle connection errors", () => {
|
it("should handle immediate connection errors", () => {
|
||||||
const mockCreds = {
|
const mockCreds = {
|
||||||
host: "example.com",
|
host: "localhost",
|
||||||
port: 22,
|
port: 22,
|
||||||
username: "user",
|
username: "user",
|
||||||
password: "pass"
|
password: "pass"
|
||||||
}
|
}
|
||||||
|
|
||||||
mockSSH2Client.on.mockImplementation((event, callback) => {
|
// Mock the connect method to throw an error immediately
|
||||||
if (event === "error") {
|
mockSSH2Client.connect.mockImplementation(() => {
|
||||||
callback(new Error("Connection failed"))
|
throw new Error("Spooky Error") // Immediate error
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return sshConnection.connect(mockCreds).catch(error => {
|
return sshConnection.connect(mockCreds).catch((error) => {
|
||||||
expect(error).toBeInstanceOf(SSHConnectionError)
|
expect(error).toBeInstanceOf(SSHConnectionError)
|
||||||
expect(error.message).toBe("Connection failed")
|
expect(error.message).toBe("Connection failed: Spooky Error")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should connect successfully with password", () => {
|
it("should connect successfully with password", () => {
|
||||||
const mockCreds = {
|
const mockCreds = {
|
||||||
host: "example.com",
|
host: "localhost",
|
||||||
port: 22,
|
port: 22,
|
||||||
username: "user",
|
username: "user",
|
||||||
password: "pass"
|
password: "pass"
|
||||||
}
|
}
|
||||||
|
|
||||||
mockSSH2Client.on.mockImplementation((event, callback) => {
|
|
||||||
if (event === "ready") {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return sshConnection.connect(mockCreds).then(() => {
|
return sshConnection.connect(mockCreds).then(() => {
|
||||||
expect(mockSSH2Client.connect).toHaveBeenCalledWith(
|
expect(mockSSH2Client.connect).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
host: "example.com",
|
host: mockCreds.host,
|
||||||
port: 22,
|
port: mockCreds.port,
|
||||||
username: "user",
|
username: mockCreds.username,
|
||||||
password: "pass",
|
password: mockCreds.password,
|
||||||
tryKeyboard: true
|
tryKeyboard: true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
describe("key authentication", () => {
|
|
||||||
it("should use privateKey when provided in config", () => {
|
|
||||||
mockConfig.user.privatekey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpA...etc\n-----END RSA PRIVATE KEY-----"
|
|
||||||
sshConnection = new SSHConnection(mockConfig)
|
|
||||||
|
|
||||||
|
it("should fail after max authentication attempts", () => {
|
||||||
const mockCreds = {
|
const mockCreds = {
|
||||||
host: "example.com",
|
host: "localhost",
|
||||||
port: 22,
|
port: 22,
|
||||||
username: "user"
|
username: "user",
|
||||||
|
password: "wrongpass"
|
||||||
}
|
}
|
||||||
|
|
||||||
mockSSH2Client.on.mockImplementation((event, callback) => {
|
const attempts = DEFAULTS.MAX_AUTH_ATTEMPTS + 1
|
||||||
if (event === "ready") {
|
mockSSH2Client.connect.mockImplementation(() => {
|
||||||
callback()
|
process.nextTick(() => {
|
||||||
}
|
registeredEventHandlers.error(new Error("Authentication failed"))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return sshConnection.connect(mockCreds).then(() => {
|
return sshConnection.connect(mockCreds).catch((error) => {
|
||||||
expect(mockSSH2Client.connect).toHaveBeenCalledWith(
|
expect(error).toBeInstanceOf(SSHConnectionError)
|
||||||
expect.objectContaining({
|
expect(error.message).toBe("All authentication methods failed")
|
||||||
privateKey: mockConfig.user.privatekey,
|
expect(mockSSH2Client.connect.mock.calls.length).toBe(attempts)
|
||||||
host: mockCreds.host,
|
|
||||||
port: mockCreds.port,
|
|
||||||
username: mockCreds.username
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle invalid private key format", () => {
|
describe("key authentication", () => {
|
||||||
mockConfig.user.privatekey = "invalid-key-format"
|
const validPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
sshConnection = new SSHConnection(mockConfig)
|
MIIEpTestKeyContentHere
|
||||||
|
-----END RSA PRIVATE KEY-----`
|
||||||
|
|
||||||
const mockCreds = {
|
it("should try private key first when both password and key are provided", () => {
|
||||||
host: "example.com",
|
const mockCreds = {
|
||||||
port: 22,
|
host: "localhost",
|
||||||
username: "user"
|
port: 22,
|
||||||
}
|
username: "user",
|
||||||
|
password: "pass",
|
||||||
|
privatekey: validPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
return sshConnection.connect(mockCreds).catch(error => {
|
return sshConnection.connect(mockCreds).then(() => {
|
||||||
expect(error).toBeInstanceOf(SSHConnectionError)
|
expect(mockSSH2Client.connect).toHaveBeenCalledWith(
|
||||||
expect(error.message).toBe("Invalid private key format")
|
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")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -160,55 +221,52 @@ describe("SSHConnection", () => {
|
||||||
sshConnection.conn = mockSSH2Client
|
sshConnection.conn = mockSSH2Client
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should open a shell successfully", () => {
|
it("should open shell successfully", () => {
|
||||||
const mockStream = {
|
const mockStream = {
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
stderr: { on: jest.fn() }
|
stderr: { on: jest.fn() }
|
||||||
}
|
}
|
||||||
|
|
||||||
mockSSH2Client.shell.mockImplementation((options, callback) => {
|
mockSSH2Client.shell.mockImplementation((options, callback) => {
|
||||||
callback(null, mockStream)
|
process.nextTick(() => callback(null, mockStream))
|
||||||
})
|
})
|
||||||
|
|
||||||
return sshConnection.shell().then(result => {
|
return sshConnection.shell().then((result) => {
|
||||||
expect(result).toBe(mockStream)
|
expect(result).toBe(mockStream)
|
||||||
expect(sshConnection.stream).toBe(mockStream)
|
expect(sshConnection.stream).toBe(mockStream)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle shell errors", () => {
|
it("should handle shell creation errors", () => {
|
||||||
mockSSH2Client.shell.mockImplementation((options, callback) => {
|
mockSSH2Client.shell.mockImplementation((options, callback) => {
|
||||||
callback(new Error("Shell error"))
|
process.nextTick(() => callback(new Error("Shell error")))
|
||||||
})
|
})
|
||||||
|
|
||||||
return sshConnection.shell().catch(error => {
|
return sshConnection.shell().catch((error) => {
|
||||||
expect(error.message).toBe("Shell error")
|
expect(error.message).toBe("Shell error")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("resizeTerminal", () => {
|
describe("resizeTerminal", () => {
|
||||||
it("should resize the terminal if stream exists", () => {
|
it("should resize terminal if stream exists", () => {
|
||||||
const mockStream = {
|
const mockStream = {
|
||||||
setWindow: jest.fn()
|
setWindow: jest.fn()
|
||||||
}
|
}
|
||||||
sshConnection.stream = mockStream
|
sshConnection.stream = mockStream
|
||||||
|
|
||||||
sshConnection.resizeTerminal(80, 24)
|
sshConnection.resizeTerminal(80, 24)
|
||||||
|
|
||||||
expect(mockStream.setWindow).toHaveBeenCalledWith(80, 24)
|
expect(mockStream.setWindow).toHaveBeenCalledWith(80, 24)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not resize if stream does not exist", () => {
|
it("should do nothing if stream does not exist", () => {
|
||||||
sshConnection.stream = null
|
sshConnection.stream = null
|
||||||
|
expect(() => sshConnection.resizeTerminal(80, 24)).not.toThrow()
|
||||||
sshConnection.resizeTerminal(80, 24)
|
|
||||||
// No error should be thrown
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("end", () => {
|
describe("end", () => {
|
||||||
it("should end the stream and connection", () => {
|
it("should close stream and connection", () => {
|
||||||
const mockStream = {
|
const mockStream = {
|
||||||
end: jest.fn()
|
end: jest.fn()
|
||||||
}
|
}
|
||||||
|
@ -223,12 +281,11 @@ describe("SSHConnection", () => {
|
||||||
expect(sshConnection.conn).toBeNull()
|
expect(sshConnection.conn).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle ending when stream and connection do not exist", () => {
|
it("should handle cleanup when no stream or connection exists", () => {
|
||||||
sshConnection.stream = null
|
sshConnection.stream = null
|
||||||
sshConnection.conn = null
|
sshConnection.conn = null
|
||||||
|
|
||||||
sshConnection.end()
|
expect(() => sshConnection.end()).not.toThrow()
|
||||||
// No error should be thrown
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
Loading…
Reference in a new issue