feat: update tests to use node:test and node:assert/strict and eliminate jest #383
This commit is contained in:
parent
db891ecb92
commit
c802350442
9 changed files with 969 additions and 6424 deletions
|
@ -1,14 +0,0 @@
|
|||
// jest.config.js
|
||||
export default {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)$": "<rootDir>/app/$1"
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": ["ts-jest", {
|
||||
useESM: true
|
||||
}]
|
||||
},
|
||||
extensionsToTreatAsEsm: [".ts"]
|
||||
}
|
6064
package-lock.json
generated
6064
package-lock.json
generated
File diff suppressed because it is too large
Load diff
11
package.json
11
package.json
|
@ -50,7 +50,7 @@
|
|||
"lint": "eslint app",
|
||||
"lint:fix": "eslint app --fix",
|
||||
"watch": "NODE_ENV=development DEBUG=webssh* node --watch index.js",
|
||||
"test": "jest",
|
||||
"test": "node --test tests/*.test.js",
|
||||
"release": "standard-version -a -s --release-as patch --commit-all",
|
||||
"release:dry-run": "standard-version -a -s --release-as patch --dry-run",
|
||||
"publish:dry-run": "npm publish --dry-run",
|
||||
|
@ -61,12 +61,6 @@
|
|||
"release:minor": "npm run release -- --release-as minor",
|
||||
"release:patch": "npm run release -- --release-as patch"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"**/tests/**/*.test.js"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"bin/*",
|
||||
|
@ -83,12 +77,11 @@
|
|||
"@types/validator": "^13.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.18.0",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-security": "^3.0.1",
|
||||
"jest": "^21.2.1",
|
||||
"prettier": "^3.4.2",
|
||||
"standard-version": "^4.4.0",
|
||||
"ts-node": "^10.9.2",
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
// server
|
||||
// tests/crypto-utils.test.js
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { generateSecureSecret } from '../app/crypto-utils.js'
|
||||
|
||||
const { generateSecureSecret } = require("../app/crypto-utils")
|
||||
|
||||
describe("generateSecureSecret", () => {
|
||||
it("should generate a 64-character hex string", () => {
|
||||
test('generateSecureSecret', async (t) => {
|
||||
await t.test('should generate a 64-character hex string', () => {
|
||||
const secret = generateSecureSecret()
|
||||
expect(secret).toMatch(/^[0-9a-f]{64}$/)
|
||||
assert.match(secret, /^[0-9a-f]{64}$/)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,88 +1,81 @@
|
|||
const {
|
||||
WebSSH2Error,
|
||||
ConfigError,
|
||||
SSHConnectionError,
|
||||
handleError
|
||||
} = require("../app/errors")
|
||||
const { logError } = require("../app/logger")
|
||||
const { HTTP, MESSAGES } = require("../app/constants")
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { WebSSH2Error, ConfigError, SSHConnectionError, handleError } from '../app/errors.js'
|
||||
import { logError } from '../app/logger.js'
|
||||
import { HTTP, MESSAGES } from '../app/constants.js'
|
||||
|
||||
jest.mock("../app/logger", () => ({
|
||||
logError: jest.fn(),
|
||||
createNamespacedDebug: jest.fn(() => jest.fn())
|
||||
}))
|
||||
// Mock logger
|
||||
const mockLogError = () => {}
|
||||
|
||||
describe("errors", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
test('errors', async (t) => {
|
||||
t.beforeEach(() => {
|
||||
// Reset mocks between tests
|
||||
})
|
||||
|
||||
describe("WebSSH2Error", () => {
|
||||
it("should create a WebSSH2Error with correct properties", () => {
|
||||
const error = new WebSSH2Error("Test error", "TEST_CODE")
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(error.name).toBe("WebSSH2Error")
|
||||
expect(error.message).toBe("Test error")
|
||||
expect(error.code).toBe("TEST_CODE")
|
||||
await t.test('WebSSH2Error', async (t) => {
|
||||
await t.test('should create WebSSH2Error with correct properties', () => {
|
||||
const error = new WebSSH2Error('Test error', 'TEST_CODE')
|
||||
assert.ok(error instanceof Error)
|
||||
assert.equal(error.name, 'WebSSH2Error')
|
||||
assert.equal(error.message, 'Test error')
|
||||
assert.equal(error.code, 'TEST_CODE')
|
||||
})
|
||||
})
|
||||
|
||||
describe("ConfigError", () => {
|
||||
it("should create a ConfigError with correct properties", () => {
|
||||
const error = new ConfigError("Config error")
|
||||
expect(error).toBeInstanceOf(WebSSH2Error)
|
||||
expect(error.name).toBe("ConfigError")
|
||||
expect(error.message).toBe("Config error")
|
||||
expect(error.code).toBe(MESSAGES.CONFIG_ERROR)
|
||||
await t.test('ConfigError', async (t) => {
|
||||
await t.test('should create ConfigError with correct properties', () => {
|
||||
const error = new ConfigError('Config error')
|
||||
assert.ok(error instanceof WebSSH2Error)
|
||||
assert.equal(error.name, 'ConfigError')
|
||||
assert.equal(error.message, 'Config error')
|
||||
assert.equal(error.code, MESSAGES.CONFIG_ERROR)
|
||||
})
|
||||
})
|
||||
|
||||
describe("SSHConnectionError", () => {
|
||||
it("should create a SSHConnectionError with correct properties", () => {
|
||||
const error = new SSHConnectionError("SSH connection error")
|
||||
expect(error).toBeInstanceOf(WebSSH2Error)
|
||||
expect(error.name).toBe("SSHConnectionError")
|
||||
expect(error.message).toBe("SSH connection error")
|
||||
expect(error.code).toBe(MESSAGES.SSH_CONNECTION_ERROR)
|
||||
await t.test('SSHConnectionError', async (t) => {
|
||||
await t.test('should create SSHConnectionError with correct properties', () => {
|
||||
const error = new SSHConnectionError('SSH connection error')
|
||||
assert.ok(error instanceof WebSSH2Error)
|
||||
assert.equal(error.name, 'SSHConnectionError')
|
||||
assert.equal(error.message, 'SSH connection error')
|
||||
assert.equal(error.code, MESSAGES.SSH_CONNECTION_ERROR)
|
||||
})
|
||||
})
|
||||
|
||||
describe("handleError", () => {
|
||||
await t.test('handleError', async (t) => {
|
||||
let responseData
|
||||
const mockRes = {
|
||||
status: jest.fn(() => mockRes),
|
||||
json: jest.fn()
|
||||
status: function(code) {
|
||||
this.statusCode = code
|
||||
return this
|
||||
},
|
||||
json: function(data) {
|
||||
responseData = data
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
it("should handle WebSSH2Error correctly", () => {
|
||||
const error = new WebSSH2Error("Test error", "TEST_CODE")
|
||||
await t.test('should handle WebSSH2Error correctly', () => {
|
||||
const error = new WebSSH2Error('Test error', 'TEST_CODE')
|
||||
handleError(error, mockRes)
|
||||
|
||||
expect(logError).toHaveBeenCalledWith("Test error", error)
|
||||
expect(mockRes.status).toHaveBeenCalledWith(HTTP.INTERNAL_SERVER_ERROR)
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: "Test error",
|
||||
code: "TEST_CODE"
|
||||
assert.deepEqual(responseData, {
|
||||
error: 'Test error',
|
||||
code: 'TEST_CODE'
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle generic Error correctly", () => {
|
||||
const error = new Error("Generic error")
|
||||
await t.test('should handle generic Error correctly', () => {
|
||||
const error = new Error('Generic error')
|
||||
handleError(error, mockRes)
|
||||
|
||||
expect(logError).toHaveBeenCalledWith(MESSAGES.UNEXPECTED_ERROR, error)
|
||||
expect(mockRes.status).toHaveBeenCalledWith(HTTP.INTERNAL_SERVER_ERROR)
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
assert.deepEqual(responseData, {
|
||||
error: MESSAGES.UNEXPECTED_ERROR
|
||||
})
|
||||
})
|
||||
|
||||
it("should not send response if res is not provided", () => {
|
||||
const error = new Error("No response error")
|
||||
handleError(error)
|
||||
|
||||
expect(logError).toHaveBeenCalledWith(MESSAGES.UNEXPECTED_ERROR, error)
|
||||
expect(mockRes.status).not.toHaveBeenCalled()
|
||||
expect(mockRes.json).not.toHaveBeenCalled()
|
||||
await t.test('should not send response if res is not provided', () => {
|
||||
const error = new Error('No response error')
|
||||
const result = handleError(error)
|
||||
assert.equal(result, undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,48 +1,33 @@
|
|||
// server
|
||||
// tests/logger.test.js
|
||||
import { test, mock } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createNamespacedDebug, logError } from '../app/logger.js'
|
||||
|
||||
const createDebug = require("debug")
|
||||
const { createNamespacedDebug, logError } = require("../app/logger")
|
||||
|
||||
jest.mock("debug")
|
||||
|
||||
describe("logger", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
console.error = jest.fn()
|
||||
})
|
||||
|
||||
describe("createNamespacedDebug", () => {
|
||||
it("should create a debug function with the correct namespace", () => {
|
||||
const mockDebug = jest.fn()
|
||||
createDebug.mockReturnValue(mockDebug)
|
||||
|
||||
const result = createNamespacedDebug("test")
|
||||
|
||||
expect(createDebug).toHaveBeenCalledWith("webssh2:test")
|
||||
expect(result).toBe(mockDebug)
|
||||
})
|
||||
})
|
||||
|
||||
describe("logError", () => {
|
||||
it("should log an error message without an error object", () => {
|
||||
const message = "Test error message"
|
||||
|
||||
logError(message)
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(message)
|
||||
expect(console.error).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should log an error message with an error object", () => {
|
||||
const message = "Test error message"
|
||||
const error = new Error("Test error")
|
||||
|
||||
logError(message, error)
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(message)
|
||||
expect(console.error).toHaveBeenCalledWith("ERROR: Error: Test error")
|
||||
expect(console.error).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
test('createNamespacedDebug creates debug function with correct namespace', (t) => {
|
||||
const debug = createNamespacedDebug('test')
|
||||
assert.equal(typeof debug, 'function')
|
||||
assert.equal(debug.namespace, 'webssh2:test')
|
||||
})
|
||||
|
||||
test('logError logs error message without error object', (t) => {
|
||||
const consoleMock = mock.method(console, 'error')
|
||||
|
||||
logError('test message')
|
||||
|
||||
assert.equal(consoleMock.mock.calls.length, 1)
|
||||
assert.deepEqual(consoleMock.mock.calls[0].arguments, ['test message'])
|
||||
|
||||
consoleMock.mock.restore()
|
||||
})
|
||||
|
||||
test('logError logs error message with error object', (t) => {
|
||||
const consoleMock = mock.method(console, 'error')
|
||||
const testError = new Error('test error')
|
||||
|
||||
logError('test message', testError)
|
||||
|
||||
assert.equal(consoleMock.mock.calls.length, 2)
|
||||
assert.deepEqual(consoleMock.mock.calls[0].arguments, ['test message'])
|
||||
assert.deepEqual(consoleMock.mock.calls[1].arguments, [`ERROR: ${testError}`])
|
||||
|
||||
consoleMock.mock.restore()
|
||||
})
|
||||
|
|
|
@ -1,95 +1,86 @@
|
|||
// server
|
||||
// tests/socket.test.js
|
||||
import { describe, it, beforeEach, mock } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import socketHandler from '../app/socket.js'
|
||||
|
||||
const EventEmitter = require("events")
|
||||
const socketHandler = require("../app/socket")
|
||||
// const WebSSH2Socket = require("../app/socket")
|
||||
|
||||
jest.mock("../app/ssh")
|
||||
|
||||
describe("socketHandler", () => {
|
||||
let io
|
||||
let socket
|
||||
let config
|
||||
describe('Socket Handler', () => {
|
||||
let io, mockSocket, mockConfig
|
||||
|
||||
beforeEach(() => {
|
||||
socket = new EventEmitter()
|
||||
socket.id = "test-socket-id"
|
||||
socket.handshake = {
|
||||
session: {}
|
||||
}
|
||||
socket.emit = jest.fn()
|
||||
// Mock Socket.IO instance
|
||||
io = new EventEmitter()
|
||||
io.on = mock.fn(io.on)
|
||||
|
||||
io = {
|
||||
on: jest.fn((event, callback) => {
|
||||
if (event === "connection") {
|
||||
callback(socket)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
config = {
|
||||
ssh: {
|
||||
term: "xterm-color"
|
||||
},
|
||||
options: {
|
||||
allowreauth: true
|
||||
// Mock socket instance
|
||||
mockSocket = new EventEmitter()
|
||||
mockSocket.id = 'test-socket-id'
|
||||
mockSocket.handshake = {
|
||||
session: {
|
||||
save: mock.fn((cb) => cb())
|
||||
}
|
||||
}
|
||||
mockSocket.emit = mock.fn()
|
||||
mockSocket.disconnect = mock.fn()
|
||||
|
||||
socketHandler(io, config)
|
||||
// Mock config
|
||||
mockConfig = {
|
||||
ssh: {
|
||||
term: 'xterm-color',
|
||||
readyTimeout: 20000,
|
||||
keepaliveInterval: 120000,
|
||||
keepaliveCountMax: 10
|
||||
},
|
||||
options: {
|
||||
allowReauth: true,
|
||||
allowReplay: true,
|
||||
allowReconnect: true
|
||||
},
|
||||
user: {}
|
||||
}
|
||||
|
||||
// Initialize socket handler
|
||||
socketHandler(io, mockConfig)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
it('should set up connection listener on io instance', () => {
|
||||
assert.equal(io.on.mock.calls.length, 1)
|
||||
assert.equal(io.on.mock.calls[0].arguments[0], 'connection')
|
||||
assert.equal(typeof io.on.mock.calls[0].arguments[1], 'function')
|
||||
})
|
||||
|
||||
test("should set up connection listener on io", () => {
|
||||
expect(io.on).toHaveBeenCalledWith("connection", expect.any(Function))
|
||||
})
|
||||
|
||||
test("should set up authenticate event listener on socket", () => {
|
||||
expect(socket.listeners("authenticate")).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("should set up terminal event listener on socket", () => {
|
||||
expect(socket.listeners("terminal")).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("should set up disconnect event listener on socket", () => {
|
||||
expect(socket.listeners("disconnect")).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("should emit request_auth when not authenticated", () => {
|
||||
expect(socket.emit).toHaveBeenCalledWith("authentication", {
|
||||
action: "request_auth"
|
||||
it('should create new WebSSH2Socket instance on connection', () => {
|
||||
const connectionHandler = io.on.mock.calls[0].arguments[1]
|
||||
connectionHandler(mockSocket)
|
||||
|
||||
// Verify socket emits authentication request when no basic auth
|
||||
assert.equal(mockSocket.emit.mock.calls[0].arguments[0], 'authentication')
|
||||
assert.deepEqual(mockSocket.emit.mock.calls[0].arguments[1], {
|
||||
action: 'request_auth'
|
||||
})
|
||||
})
|
||||
|
||||
test("should handle authenticate event", () => {
|
||||
const creds = {
|
||||
username: "testuser",
|
||||
password: "testpass",
|
||||
host: "testhost",
|
||||
port: 22
|
||||
}
|
||||
socket.emit("authenticate", creds)
|
||||
// build out later
|
||||
})
|
||||
|
||||
test("should handle terminal event", () => {
|
||||
const terminalData = {
|
||||
term: "xterm",
|
||||
rows: 24,
|
||||
cols: 80
|
||||
}
|
||||
socket.emit("terminal", terminalData)
|
||||
// build out later
|
||||
})
|
||||
|
||||
test("should handle disconnect event", () => {
|
||||
const reason = "test-reason"
|
||||
socket.emit("disconnect", reason)
|
||||
// build out later
|
||||
})
|
||||
})
|
||||
|
||||
describe('Authentication Flow', () => {
|
||||
it.todo('should handle keyboard-interactive authentication')
|
||||
it.todo('should process successful authentication')
|
||||
it.todo('should handle invalid credentials')
|
||||
it.todo('should respect disableInteractiveAuth setting')
|
||||
})
|
||||
|
||||
describe('Terminal Operations', () => {
|
||||
it.todo('should handle terminal resize events')
|
||||
it.todo('should process terminal data correctly')
|
||||
it.todo('should maintain terminal session state')
|
||||
})
|
||||
|
||||
describe('Control Commands', () => {
|
||||
it.todo('should process reauth commands')
|
||||
it.todo('should handle credential replay')
|
||||
it.todo('should update UI elements appropriately')
|
||||
})
|
||||
|
||||
describe('Session Management', () => {
|
||||
it.todo('should clean up on disconnect')
|
||||
it.todo('should manage session state')
|
||||
it.todo('should clear credentials properly')
|
||||
})
|
||||
|
|
|
@ -1,291 +1,268 @@
|
|||
/* eslint-disable jest/no-conditional-expect */
|
||||
// server
|
||||
// tests/ssh.test.js
|
||||
import ssh2 from 'ssh2'
|
||||
import crypto from 'crypto'
|
||||
import { test, describe, beforeEach, afterEach } from 'node:test'
|
||||
import { strict as assert } from 'assert'
|
||||
const { Server } = ssh2
|
||||
import SSHConnection from '../app/ssh.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", () => {
|
||||
describe('SSHConnection', () => {
|
||||
let sshServer
|
||||
let sshConnection
|
||||
let mockConfig
|
||||
let mockSSH2Client
|
||||
let registeredEventHandlers
|
||||
const TEST_PORT = 2222
|
||||
const TEST_CREDENTIALS = {
|
||||
username: 'testuser',
|
||||
password: 'testpass',
|
||||
}
|
||||
|
||||
const { privateKey } = crypto.generateKeyPairSync('rsa', {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs1',
|
||||
format: 'pem',
|
||||
},
|
||||
})
|
||||
|
||||
const mockConfig = {
|
||||
ssh: {
|
||||
algorithms: {},
|
||||
readyTimeout: 2000,
|
||||
keepaliveInterval: 1000,
|
||||
keepaliveCountMax: 3,
|
||||
term: 'xterm',
|
||||
},
|
||||
user: {
|
||||
privateKey: null,
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
sshServer = new Server(
|
||||
{
|
||||
hostKeys: [privateKey],
|
||||
},
|
||||
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()
|
||||
(client) => {
|
||||
client.on('authentication', (ctx) => {
|
||||
if (
|
||||
ctx.method === 'password' &&
|
||||
ctx.username === TEST_CREDENTIALS.username &&
|
||||
ctx.password === TEST_CREDENTIALS.password
|
||||
) {
|
||||
ctx.accept()
|
||||
} else {
|
||||
ctx.reject()
|
||||
}
|
||||
})
|
||||
}),
|
||||
shell: jest.fn(),
|
||||
end: jest.fn()
|
||||
}
|
||||
|
||||
SSH2.Client.mockImplementation(() => mockSSH2Client)
|
||||
client.on('ready', () => {
|
||||
client.on('session', (accept) => {
|
||||
const session = accept()
|
||||
session.once('pty', (accept) => {
|
||||
accept()
|
||||
})
|
||||
session.once('shell', (accept) => {
|
||||
const stream = accept()
|
||||
stream.write('Connected to test server\r\n')
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
sshServer.listen(TEST_PORT)
|
||||
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
|
||||
|
||||
if (sshConnection) {
|
||||
sshConnection.end()
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
sshServer.close(resolve)
|
||||
})
|
||||
})
|
||||
|
||||
expect(mockStream.end).toHaveBeenCalled()
|
||||
expect(mockSSH2Client.end).toHaveBeenCalled()
|
||||
expect(sshConnection.stream).toBeNull()
|
||||
expect(sshConnection.conn).toBeNull()
|
||||
test('should connect with valid credentials', async () => {
|
||||
const credentials = {
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
username: TEST_CREDENTIALS.username,
|
||||
password: TEST_CREDENTIALS.password,
|
||||
}
|
||||
|
||||
const connection = await sshConnection.connect(credentials)
|
||||
assert.ok(connection, 'Connection should be established')
|
||||
})
|
||||
|
||||
test('should reject connection with invalid credentials', async () => {
|
||||
const invalidCredentials = {
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
username: 'wronguser',
|
||||
password: 'wrongpass',
|
||||
}
|
||||
|
||||
try {
|
||||
await sshConnection.connect(invalidCredentials)
|
||||
assert.fail('Connection should have been rejected')
|
||||
} catch (error) {
|
||||
assert.equal(error.name, 'SSHConnectionError')
|
||||
assert.equal(error.message, 'All authentication methods failed')
|
||||
}
|
||||
})
|
||||
|
||||
test('should connect using private key authentication', async () => {
|
||||
const credentials = {
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
username: TEST_CREDENTIALS.username,
|
||||
privateKey: privateKey,
|
||||
}
|
||||
|
||||
// Update server auth handler to accept private key
|
||||
sshServer.removeAllListeners('connection')
|
||||
sshServer.on('connection', (client) => {
|
||||
client.on('authentication', (ctx) => {
|
||||
if (ctx.method === 'publickey' && ctx.username === TEST_CREDENTIALS.username) {
|
||||
ctx.accept()
|
||||
} else {
|
||||
ctx.reject()
|
||||
}
|
||||
})
|
||||
|
||||
client.on('ready', () => {
|
||||
client.on('session', (accept) => {
|
||||
accept()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle cleanup when no stream or connection exists", () => {
|
||||
sshConnection.stream = null
|
||||
sshConnection.conn = null
|
||||
const connection = await sshConnection.connect(credentials)
|
||||
assert.ok(connection, 'Connection should be established using private key')
|
||||
})
|
||||
|
||||
expect(() => sshConnection.end()).not.toThrow()
|
||||
test('should reject invalid private key format', async () => {
|
||||
const invalidPrivateKey = 'not-a-valid-private-key-format'
|
||||
const credentials = {
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
username: TEST_CREDENTIALS.username,
|
||||
privateKey: invalidPrivateKey,
|
||||
}
|
||||
|
||||
try {
|
||||
await sshConnection.connect(credentials)
|
||||
assert.fail('Connection should have been rejected')
|
||||
} catch (error) {
|
||||
assert.equal(error.name, 'SSHConnectionError')
|
||||
assert.equal(error.message, 'Invalid private key format')
|
||||
}
|
||||
})
|
||||
test('should resize terminal when stream exists', async () => {
|
||||
const credentials = {
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
username: TEST_CREDENTIALS.username,
|
||||
password: TEST_CREDENTIALS.password,
|
||||
}
|
||||
|
||||
// Connect and create shell
|
||||
await sshConnection.connect(credentials)
|
||||
await sshConnection.shell({ term: 'xterm' })
|
||||
|
||||
// Mock the setWindow method on stream
|
||||
let windowResized = false
|
||||
sshConnection.stream.setWindow = (rows, cols) => {
|
||||
windowResized = true
|
||||
assert.equal(rows, 24)
|
||||
assert.equal(cols, 80)
|
||||
}
|
||||
|
||||
// Test resize
|
||||
sshConnection.resizeTerminal(24, 80)
|
||||
assert.ok(windowResized, 'Terminal should be resized')
|
||||
})
|
||||
test('should try private key first when both password and key are provided', async () => {
|
||||
const authAttemptOrder = []
|
||||
|
||||
sshServer.removeAllListeners('connection')
|
||||
sshServer.on('connection', (client) => {
|
||||
client.on('authentication', (ctx) => {
|
||||
authAttemptOrder.push(ctx.method)
|
||||
|
||||
if (ctx.method === 'publickey' && ctx.username === TEST_CREDENTIALS.username) {
|
||||
return ctx.accept()
|
||||
}
|
||||
|
||||
if (
|
||||
ctx.method === 'password' &&
|
||||
ctx.username === TEST_CREDENTIALS.username &&
|
||||
ctx.password === TEST_CREDENTIALS.password
|
||||
) {
|
||||
return ctx.accept()
|
||||
}
|
||||
|
||||
ctx.reject(['publickey', 'password'])
|
||||
})
|
||||
|
||||
client.on('ready', () => {
|
||||
client.on('session', (accept) => {
|
||||
const session = accept()
|
||||
session.once('pty', (accept) => accept())
|
||||
session.once('shell', (accept) => accept())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const credentials = {
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
username: TEST_CREDENTIALS.username,
|
||||
privateKey: privateKey,
|
||||
password: TEST_CREDENTIALS.password,
|
||||
}
|
||||
|
||||
const connection = await sshConnection.connect(credentials)
|
||||
assert.ok(connection, 'Connection should be established')
|
||||
assert.ok(
|
||||
authAttemptOrder.includes('publickey'),
|
||||
'Private key authentication should be attempted'
|
||||
)
|
||||
})
|
||||
test('should handle connection failures', async () => {
|
||||
const credentials = {
|
||||
host: 'localhost',
|
||||
port: 9999,
|
||||
username: TEST_CREDENTIALS.username,
|
||||
password: TEST_CREDENTIALS.password,
|
||||
}
|
||||
|
||||
try {
|
||||
await sshConnection.connect(credentials)
|
||||
assert.fail('Connection should have failed')
|
||||
} catch (error) {
|
||||
assert.equal(error.name, 'SSHConnectionError')
|
||||
assert.equal(error.message, 'All authentication methods failed')
|
||||
}
|
||||
})
|
||||
|
||||
test('should handle connection timeout', async () => {
|
||||
const credentials = {
|
||||
host: '240.0.0.0',
|
||||
port: TEST_PORT,
|
||||
username: TEST_CREDENTIALS.username,
|
||||
password: TEST_CREDENTIALS.password,
|
||||
}
|
||||
|
||||
try {
|
||||
await sshConnection.connect(credentials)
|
||||
assert.fail('Connection should have timed out')
|
||||
} catch (error) {
|
||||
assert.equal(error.name, 'SSHConnectionError')
|
||||
assert.equal(error.message, 'All authentication methods failed')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// server
|
||||
// tests/utils.test.js
|
||||
|
||||
const {
|
||||
import { test, describe } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
deepMerge,
|
||||
getValidatedHost,
|
||||
getValidatedPort,
|
||||
|
@ -9,214 +11,221 @@ const {
|
|||
maskSensitiveData,
|
||||
modifyHtml,
|
||||
validateConfig,
|
||||
validateSshTerm
|
||||
} = require("../app/utils")
|
||||
validateSshTerm,
|
||||
} from '../app/utils.js'
|
||||
|
||||
describe("utils", () => {
|
||||
describe("deepMerge", () => {
|
||||
it("should merge two objects deeply", () => {
|
||||
const obj1 = { a: { b: 1 }, c: 2 }
|
||||
const obj2 = { a: { d: 3 }, e: 4 }
|
||||
const result = deepMerge(obj1, obj2)
|
||||
expect(result).toEqual({ a: { b: 1, d: 3 }, c: 2, e: 4 })
|
||||
})
|
||||
})
|
||||
|
||||
describe("getValidatedHost", () => {
|
||||
it("should return IP address unchanged", () => {
|
||||
expect(getValidatedHost("192.168.1.1")).toBe("192.168.1.1")
|
||||
})
|
||||
|
||||
it("should escape hostname", () => {
|
||||
expect(getValidatedHost("example.com")).toBe("example.com")
|
||||
expect(getValidatedHost("<script>alert('xss')</script>")).toBe(
|
||||
"<script>alert('xss')</script>"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getValidatedPort", () => {
|
||||
it("should return valid port number", () => {
|
||||
expect(getValidatedPort("22")).toBe(22)
|
||||
expect(getValidatedPort("8080")).toBe(8080)
|
||||
})
|
||||
|
||||
it("should return default port for invalid input", () => {
|
||||
expect(getValidatedPort("invalid")).toBe(22)
|
||||
expect(getValidatedPort("0")).toBe(22)
|
||||
expect(getValidatedPort("65536")).toBe(22)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isValidCredentials", () => {
|
||||
it("should return true for valid credentials", () => {
|
||||
const validCreds = {
|
||||
username: "user",
|
||||
password: "pass",
|
||||
host: "example.com",
|
||||
port: 22
|
||||
}
|
||||
expect(isValidCredentials(validCreds)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for invalid credentials", () => {
|
||||
expect(isValidCredentials(null)).toBe(false)
|
||||
expect(isValidCredentials({})).toBe(false)
|
||||
expect(isValidCredentials({ username: "user" })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("maskSensitiveData", () => {
|
||||
it("should mask simple password property", () => {
|
||||
const testObj = { username: "user", password: "secret123" }
|
||||
const maskedObj = maskSensitiveData(testObj)
|
||||
console.log("maskedObj.password.length: ", maskedObj.password.length)
|
||||
|
||||
expect(maskedObj.username).toBe("user")
|
||||
expect(maskedObj.password).not.toBe("secret123")
|
||||
expect(maskedObj.password.length).toBeGreaterThanOrEqual(3)
|
||||
expect(maskedObj.password.length).toBeLessThanOrEqual(9)
|
||||
})
|
||||
|
||||
it("should mask array elements when property is specified", () => {
|
||||
const testObj = {
|
||||
action: "keyboard-interactive",
|
||||
responses: ["sensitive_password", "another_sensitive_value"]
|
||||
}
|
||||
const maskedObj = maskSensitiveData(testObj, {
|
||||
properties: ["responses"]
|
||||
})
|
||||
|
||||
expect(maskedObj.action).toBe("keyboard-interactive")
|
||||
expect(Array.isArray(maskedObj.responses)).toBe(true)
|
||||
expect(maskedObj.responses).toHaveLength(2)
|
||||
expect(maskedObj.responses[0]).not.toBe("sensitive_password")
|
||||
expect(maskedObj.responses[1]).not.toBe("another_sensitive_value")
|
||||
expect(maskedObj.responses[0]).toHaveLength(8)
|
||||
expect(maskedObj.responses[1]).toHaveLength(8)
|
||||
})
|
||||
|
||||
it("should not mask non-specified properties", () => {
|
||||
const testObj = {
|
||||
username: "user",
|
||||
password: "secret",
|
||||
data: ["public_info", "not_sensitive"]
|
||||
}
|
||||
const maskedObj = maskSensitiveData(testObj, {
|
||||
properties: ["password"]
|
||||
})
|
||||
|
||||
expect(maskedObj.username).toBe("user")
|
||||
expect(maskedObj.password).not.toBe("secret")
|
||||
expect(maskedObj.data).toEqual(["public_info", "not_sensitive"])
|
||||
})
|
||||
|
||||
it("should handle nested objects", () => {
|
||||
const testObj = {
|
||||
user: {
|
||||
name: "John",
|
||||
credentials: {
|
||||
password: "topsecret",
|
||||
token: "abcdef123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
const maskedObj = maskSensitiveData(testObj)
|
||||
|
||||
expect(maskedObj.user.name).toBe("John")
|
||||
expect(maskedObj.user.credentials.password).not.toBe("topsecret")
|
||||
expect(maskedObj.user.credentials.token).not.toBe("abcdef123456")
|
||||
})
|
||||
})
|
||||
|
||||
describe("modifyHtml", () => {
|
||||
it("should modify HTML content", () => {
|
||||
const html = "window.webssh2Config = null;"
|
||||
const config = { key: "value" }
|
||||
const content = `window.webssh2Config = ${JSON.stringify(config)};`
|
||||
const modified = modifyHtml(html, config)
|
||||
expect(modified).toContain('window.webssh2Config = {"key":"value"};')
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateConfig", () => {
|
||||
it("should validate correct config", () => {
|
||||
const validConfig = {
|
||||
listen: {
|
||||
ip: "0.0.0.0",
|
||||
port: 2222
|
||||
},
|
||||
http: {
|
||||
origins: ["http://localhost:8080"]
|
||||
},
|
||||
user: {
|
||||
name: null,
|
||||
password: null,
|
||||
privateKey: null
|
||||
},
|
||||
ssh: {
|
||||
host: null,
|
||||
port: 22,
|
||||
localAddress: null,
|
||||
localPort: null,
|
||||
term: "xterm-color",
|
||||
readyTimeout: 20000,
|
||||
keepaliveInterval: 120000,
|
||||
keepaliveCountMax: 10,
|
||||
allowedSubnets: []
|
||||
},
|
||||
header: {
|
||||
text: null,
|
||||
background: "green"
|
||||
},
|
||||
options: {
|
||||
challengeButton: true,
|
||||
autoLog: false,
|
||||
allowReauth: true,
|
||||
allowReconnect: true,
|
||||
allowReplay: true
|
||||
},
|
||||
algorithms: {
|
||||
kex: [
|
||||
"ecdh-sha2-nistp256",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp521",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group14-sha1"
|
||||
],
|
||||
cipher: [
|
||||
"aes128-ctr",
|
||||
"aes192-ctr",
|
||||
"aes256-ctr",
|
||||
"aes128-gcm",
|
||||
"aes128-gcm@openssh.com",
|
||||
"aes256-gcm",
|
||||
"aes256-gcm@openssh.com",
|
||||
"aes256-cbc"
|
||||
],
|
||||
hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"]
|
||||
}
|
||||
}
|
||||
|
||||
expect(() => validateConfig(validConfig)).not.toThrow()
|
||||
})
|
||||
|
||||
it("should throw error for invalid config", () => {
|
||||
const invalidConfig = {}
|
||||
expect(() => validateConfig(invalidConfig)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateSshTerm", () => {
|
||||
it("should return valid SSH term", () => {
|
||||
expect(validateSshTerm("xterm")).toBe("xterm")
|
||||
expect(validateSshTerm("xterm-256color")).toBe("xterm-256color")
|
||||
})
|
||||
|
||||
it("should return null for invalid SSH term", () => {
|
||||
expect(validateSshTerm("")).toBe(null)
|
||||
expect(validateSshTerm("<script>alert('xss')</script>")).toBe(null)
|
||||
})
|
||||
describe('deepMerge', () => {
|
||||
test('merges nested objects correctly', () => {
|
||||
const target = { a: { b: 1 }, c: 3 }
|
||||
const source = { a: { d: 2 }, e: 4 }
|
||||
const result = deepMerge(target, source)
|
||||
assert.deepEqual(result, { a: { b: 1, d: 2 }, c: 3, e: 4 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('getValidatedHost', () => {
|
||||
test('returns valid IP unchanged', () => {
|
||||
assert.equal(getValidatedHost('192.168.1.1'), '192.168.1.1')
|
||||
})
|
||||
|
||||
test('escapes hostname with potential XSS', () => {
|
||||
assert.equal(
|
||||
getValidatedHost('host<script>alert(1)</script>'),
|
||||
'host<script>alert(1)</script>'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getValidatedPort', () => {
|
||||
test('returns valid port number', () => {
|
||||
assert.equal(getValidatedPort('22'), 22)
|
||||
})
|
||||
|
||||
test('returns default port for invalid input', () => {
|
||||
assert.equal(getValidatedPort('0'), 22)
|
||||
assert.equal(getValidatedPort('65536'), 22)
|
||||
assert.equal(getValidatedPort('invalid'), 22)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidCredentials', () => {
|
||||
test('validates complete credentials', () => {
|
||||
const validCreds = {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
host: 'localhost',
|
||||
port: 22,
|
||||
}
|
||||
assert.equal(isValidCredentials(validCreds), true)
|
||||
})
|
||||
|
||||
test('rejects incomplete credentials', () => {
|
||||
const invalidCreds = {
|
||||
username: 'user',
|
||||
host: 'localhost',
|
||||
}
|
||||
assert.equal(isValidCredentials(invalidCreds), false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('maskSensitiveData', () => {
|
||||
test('masks password in object', () => {
|
||||
const input = { username: 'user', password: 'secret' }
|
||||
const masked = maskSensitiveData(input)
|
||||
assert.equal(masked.password.includes('*'), true)
|
||||
assert.equal(masked.username, 'user')
|
||||
})
|
||||
|
||||
test('masks nested sensitive data', () => {
|
||||
const input = {
|
||||
user: {
|
||||
credentials: {
|
||||
password: 'secret',
|
||||
},
|
||||
},
|
||||
}
|
||||
const masked = maskSensitiveData(input)
|
||||
assert.equal(masked.user.credentials.password.includes('*'), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('modifyHtml', () => {
|
||||
test('injects config and modifies asset paths', () => {
|
||||
const html = `
|
||||
<script src="script.js"></script>
|
||||
<script>window.webssh2Config = null;</script>
|
||||
`
|
||||
const config = { key: 'value' }
|
||||
const modified = modifyHtml(html, config)
|
||||
assert.ok(modified.includes('/ssh/assets/script.js'))
|
||||
assert.ok(modified.includes('window.webssh2Config = {"key":"value"}'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateConfig', () => {
|
||||
test('validates correct config', () => {
|
||||
const validConfig = {
|
||||
listen: {
|
||||
ip: '0.0.0.0',
|
||||
port: 2222,
|
||||
},
|
||||
http: {
|
||||
origins: ['http://localhost:2222'],
|
||||
},
|
||||
user: {
|
||||
name: 'testuser',
|
||||
password: 'testpass',
|
||||
privateKey: null,
|
||||
},
|
||||
ssh: {
|
||||
host: 'localhost',
|
||||
port: 22,
|
||||
term: 'xterm',
|
||||
readyTimeout: 20000,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 10,
|
||||
algorithms: {
|
||||
kex: ['ecdh-sha2-nistp256'],
|
||||
cipher: ['aes256-ctr'],
|
||||
hmac: ['hmac-sha2-256'],
|
||||
serverHostKey: ['ssh-rsa'],
|
||||
compress: ['none'],
|
||||
},
|
||||
},
|
||||
header: {
|
||||
text: 'WebSSH2',
|
||||
background: 'green',
|
||||
},
|
||||
options: {
|
||||
challengeButton: false,
|
||||
allowReauth: true,
|
||||
allowReplay: true,
|
||||
},
|
||||
session: {
|
||||
secret: 'mysecret',
|
||||
name: 'webssh2',
|
||||
},
|
||||
}
|
||||
assert.doesNotThrow(() => validateConfig(validConfig))
|
||||
})
|
||||
|
||||
test('throws on missing required fields', () => {
|
||||
const invalidConfig = {
|
||||
listen: { ip: '0.0.0.0' }, // missing required port
|
||||
http: { origins: [] },
|
||||
user: { name: 'test' }, // missing required password
|
||||
ssh: {
|
||||
host: 'localhost',
|
||||
port: 22,
|
||||
term: 'xterm',
|
||||
// missing required fields
|
||||
},
|
||||
header: {
|
||||
text: null,
|
||||
// missing required background
|
||||
},
|
||||
options: {
|
||||
// missing required fields
|
||||
},
|
||||
// missing required session
|
||||
}
|
||||
assert.throws(() => validateConfig(invalidConfig))
|
||||
})
|
||||
|
||||
test('throws on invalid field types', () => {
|
||||
const invalidTypeConfig = {
|
||||
listen: {
|
||||
ip: 123, // should be string
|
||||
port: '2222', // should be integer
|
||||
},
|
||||
http: {
|
||||
origins: 'not-an-array', // should be array
|
||||
},
|
||||
user: {
|
||||
name: true, // should be string or null
|
||||
password: 123, // should be string or null
|
||||
},
|
||||
ssh: {
|
||||
host: null,
|
||||
port: 'invalid-port', // should be integer
|
||||
term: 123, // should be string
|
||||
readyTimeout: '1000', // should be integer
|
||||
keepaliveInterval: false, // should be integer
|
||||
keepaliveCountMax: [], // should be integer
|
||||
algorithms: {
|
||||
kex: 'not-an-array', // should be array
|
||||
cipher: 'not-an-array', // should be array
|
||||
hmac: 'not-an-array', // should be array
|
||||
serverHostKey: 'not-an-array', // should be array
|
||||
compress: 'not-an-array', // should be array
|
||||
},
|
||||
},
|
||||
header: {
|
||||
text: 123, // should be string or null
|
||||
background: true, // should be string
|
||||
},
|
||||
options: {
|
||||
challengeButton: 'not-boolean', // should be boolean
|
||||
allowReauth: 1, // should be boolean
|
||||
allowReplay: 'true', // should be boolean
|
||||
},
|
||||
session: {
|
||||
secret: null, // should be string
|
||||
name: [], // should be string
|
||||
},
|
||||
}
|
||||
assert.throws(() => validateConfig(invalidTypeConfig))
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateSshTerm', () => {
|
||||
test('validates legitimate terminal types', () => {
|
||||
assert.equal(validateSshTerm('xterm'), 'xterm')
|
||||
assert.equal(validateSshTerm('xterm-256color'), 'xterm-256color')
|
||||
})
|
||||
|
||||
test('returns null for invalid terminal strings', () => {
|
||||
assert.equal(validateSshTerm('<script>alert(1)</script>'), null)
|
||||
assert.equal(validateSshTerm(''), null)
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue