Problem: static_enums was a manually maintained table of ~40 keywords
that could go stale when OpenSSH adds or changes config options.
Solution: parse enum values directly from the ssh_config man page using
two strategies: definitive list sentences ("The argument must be X, Y,
or Z") and fallback extraction from "set to X" patterns. Also fixes
keyword detection to handle digits (ForwardX11) and inline definitions
(Tunnel, SetEnv). Covers 36/41 old keywords plus discovers 4 new ones
(IPQoS, SyslogFacility, ObscureKeystrokeTiming, ControlPersist).
202 lines
6.2 KiB
Lua
202 lines
6.2 KiB
Lua
local helpers = require('spec.helpers')
|
|
|
|
local MAN_PAGE = table.concat({
|
|
'SSH_CONFIG(5) File Formats Manual SSH_CONFIG(5)',
|
|
'',
|
|
' The possible keywords and their meanings are as follows:',
|
|
'',
|
|
' Host Restricts the following declarations (up to the next Host or',
|
|
' Match keyword) to be only for those hosts that match one of the',
|
|
' patterns given after the keyword.',
|
|
'',
|
|
' StrictHostKeyChecking',
|
|
' If this flag is set to yes, ssh(1) will never automatically add',
|
|
' host keys to the ~/.ssh/known_hosts file, and refuses to connect',
|
|
' to hosts whose host key has changed. If this flag is set to',
|
|
' accept-new then ssh will automatically add new host keys. If',
|
|
' this flag is set to no or off, ssh will automatically add new',
|
|
' host keys. If this flag is set to ask (the default), new host',
|
|
' keys will be added only after the user has confirmed.',
|
|
'',
|
|
' Hostname',
|
|
' Specifies the real host name to log into.',
|
|
'',
|
|
}, '\n')
|
|
|
|
local SSH_Q_OUTPUT = table.concat({
|
|
'##cipher',
|
|
'aes128-ctr',
|
|
'aes256-ctr',
|
|
'chacha20-poly1305@openssh.com',
|
|
'##cipher-auth',
|
|
'chacha20-poly1305@openssh.com',
|
|
'##mac',
|
|
'hmac-sha2-256',
|
|
'##kex',
|
|
'curve25519-sha256',
|
|
'##key',
|
|
'ssh-ed25519',
|
|
'##key-cert',
|
|
'##key-plain',
|
|
'##key-sig',
|
|
'ssh-ed25519',
|
|
'##protocol-version',
|
|
'2',
|
|
'##compression',
|
|
'none',
|
|
'zlib@openssh.com',
|
|
'##sig',
|
|
}, '\n')
|
|
|
|
local function mock_system()
|
|
local original_system = vim.system
|
|
local original_schedule = vim.schedule
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
vim.system = function(cmd, _, on_exit)
|
|
local stdout = ''
|
|
if cmd[1] == 'bash' and cmd[3] and cmd[3]:find('man %-P cat ssh_config') then
|
|
stdout = MAN_PAGE
|
|
elseif cmd[1] == 'bash' and cmd[3] and cmd[3]:find('ssh %-Q') then
|
|
stdout = SSH_Q_OUTPUT
|
|
end
|
|
local result = { stdout = stdout, code = 0 }
|
|
if on_exit then
|
|
on_exit(result)
|
|
return {}
|
|
end
|
|
return {
|
|
wait = function()
|
|
return result
|
|
end,
|
|
}
|
|
end
|
|
vim.schedule = function(fn)
|
|
fn()
|
|
end
|
|
return function()
|
|
vim.system = original_system
|
|
vim.schedule = original_schedule
|
|
end
|
|
end
|
|
|
|
describe('blink-cmp-ssh', function()
|
|
local restores = {}
|
|
|
|
before_each(function()
|
|
package.loaded['blink-cmp-ssh'] = nil
|
|
end)
|
|
|
|
after_each(function()
|
|
for _, fn in ipairs(restores) do
|
|
fn()
|
|
end
|
|
restores = {}
|
|
end)
|
|
|
|
describe('enabled', function()
|
|
it('returns true for sshconfig filetype', function()
|
|
local bufnr = helpers.create_buffer({}, 'sshconfig')
|
|
local source = require('blink-cmp-ssh')
|
|
assert.is_true(source.enabled())
|
|
helpers.delete_buffer(bufnr)
|
|
end)
|
|
|
|
it('returns false for other filetypes', function()
|
|
local bufnr = helpers.create_buffer({}, 'lua')
|
|
local source = require('blink-cmp-ssh')
|
|
assert.is_false(source.enabled())
|
|
helpers.delete_buffer(bufnr)
|
|
end)
|
|
end)
|
|
|
|
describe('get_completions', function()
|
|
it('returns keyword items with Property kind on empty line', function()
|
|
restores[#restores + 1] = mock_system()
|
|
local source = require('blink-cmp-ssh').new()
|
|
local items
|
|
source:get_completions({ line = '', cursor = { 1, 0 } }, function(response)
|
|
items = response.items
|
|
end)
|
|
assert.is_not_nil(items)
|
|
assert.equals(3, #items)
|
|
for _, item in ipairs(items) do
|
|
assert.equals(10, item.kind)
|
|
end
|
|
end)
|
|
|
|
it('returns keyword items on partial keyword', function()
|
|
restores[#restores + 1] = mock_system()
|
|
local source = require('blink-cmp-ssh').new()
|
|
local items
|
|
source:get_completions({ line = 'Str', cursor = { 1, 3 } }, function(response)
|
|
items = response.items
|
|
end)
|
|
assert.is_not_nil(items)
|
|
assert.equals(3, #items)
|
|
end)
|
|
|
|
it('includes man page documentation in items', function()
|
|
restores[#restores + 1] = mock_system()
|
|
local source = require('blink-cmp-ssh').new()
|
|
local items
|
|
source:get_completions({ line = '', cursor = { 1, 0 } }, function(response)
|
|
items = response.items
|
|
end)
|
|
local strict = vim.iter(items):find(function(item)
|
|
return item.label == 'StrictHostKeyChecking'
|
|
end)
|
|
assert.is_not_nil(strict)
|
|
assert.is_not_nil(strict.documentation)
|
|
assert.is_truthy(strict.documentation.value:find('known_hosts'))
|
|
end)
|
|
|
|
it('includes inline description for keywords with text on the same line', function()
|
|
restores[#restores + 1] = mock_system()
|
|
local source = require('blink-cmp-ssh').new()
|
|
local items
|
|
source:get_completions({ line = '', cursor = { 1, 0 } }, function(response)
|
|
items = response.items
|
|
end)
|
|
local host = vim.iter(items):find(function(item)
|
|
return item.label == 'Host'
|
|
end)
|
|
assert.is_not_nil(host)
|
|
assert.is_not_nil(host.documentation)
|
|
assert.is_truthy(host.documentation.value:find('Restricts'))
|
|
end)
|
|
|
|
it('returns enum values after a known keyword', function()
|
|
restores[#restores + 1] = mock_system()
|
|
local source = require('blink-cmp-ssh').new()
|
|
local items
|
|
source:get_completions(
|
|
{ line = 'StrictHostKeyChecking ', cursor = { 1, 22 } },
|
|
function(response)
|
|
items = response.items
|
|
end
|
|
)
|
|
assert.is_not_nil(items)
|
|
assert.is_true(#items > 0)
|
|
for _, item in ipairs(items) do
|
|
assert.equals(20, item.kind)
|
|
end
|
|
end)
|
|
|
|
it('returns empty after a non-enum keyword', function()
|
|
restores[#restores + 1] = mock_system()
|
|
local source = require('blink-cmp-ssh').new()
|
|
local items
|
|
source:get_completions({ line = 'Hostname ', cursor = { 1, 9 } }, function(response)
|
|
items = response.items
|
|
end)
|
|
assert.equals(0, #items)
|
|
end)
|
|
|
|
it('returns a cancel function', function()
|
|
restores[#restores + 1] = mock_system()
|
|
local source = require('blink-cmp-ssh').new()
|
|
local cancel = source:get_completions({ line = '', cursor = { 1, 0 } }, function() end)
|
|
assert.is_function(cancel)
|
|
end)
|
|
end)
|
|
end)
|