diff --git a/runtime/lua/vim/net.lua b/runtime/lua/vim/net.lua index 6e09619615..ff3a1558f1 100644 --- a/runtime/lua/vim/net.lua +++ b/runtime/lua/vim/net.lua @@ -10,7 +10,7 @@ local http_methods = { } ---@alias vim.net.request.ResponseFunc fun(err: string?, response: vim.net.request.Response?) ----@alias vim.net.HttpMethod string "GET" | "POST" | "PUT" | "PACH" | "HEAD"| "DELETE +---@alias vim.net.HttpMethod string "GET" | "POST" | "PUT" | "PATCH" | "HEAD" | "DELETE" ---@class vim.net.request.Opts ---@inlinedoc @@ -98,8 +98,8 @@ function M.request(method, url, opts, on_response) opts = opts or {} vim.validate('method', method, function(m) - return http_methods[m] == true, ('invalid HTTP method: %s'):format(m) - end) + return http_methods[m] == true + end, 'method should be one of GET, POST, PUT, PATCH, HEAD, DELETE') vim.validate('url', url, 'string') vim.validate('opts', opts, 'table', true) vim.validate('opts.headers', opts.headers, 'table', true) diff --git a/test/functional/lua/net_spec.lua b/test/functional/lua/net_spec.lua index 9270be1fca..b272063ba0 100644 --- a/test/functional/lua/net_spec.lua +++ b/test/functional/lua/net_spec.lua @@ -4,55 +4,42 @@ local skip_integ = os.getenv('NVIM_TEST_INTEG') ~= '1' local exec_lua = n.exec_lua -local function assert_404_error(err) - assert( - err:lower():find('404') or err:find('22'), - 'Expected HTTP 404 or exit code 22, got: ' .. tostring(err) - ) -end +---@param method vim.net.HttpMethod +---@param opts? vim.net.request.Opts +---@return table +--- Helper method to make a HTTP request with a 2s timeout +local function request(method, url, opts) + opts = opts or {} + opts.retry = 3 + local result = exec_lua(function() + local done = false + local result -local function assert_wrong_headers(expected_err, header) - local err = t.pcall_err(exec_lua, [[ - return vim.net.request('https://example.com', { - headers = ]] .. header .. [[, - }, function() end) - ]]) - t.matches(expected_err, err) -end + vim.net.request(method, url, opts, function(err, res) + if err then + result = { error = err } + else + ---@type string|table + local resp -local function assert_wrong_body(expected_err, body) - local err = t.pcall_err(exec_lua, [[ - return vim.net.request('POST', 'https://example.com', { - body = ]] .. body .. [[, - }, function() end) - ]]) - t.matches(expected_err, err) -end - -local function assert_invalid_method(method) - local err = t.pcall_err(exec_lua, [[ - return vim.net.request(']] .. method .. [[', 'https://example.com', {}, function() end) - ]]) - t.matches('invalid HTTP method', err) -end - -local function request(method, opts) - return exec_lua([[ - local done = false - local result - - vim.net.request("]] .. method .. [[", "https://httpbingo.org/anything", ]] .. opts .. [[, function(err, res) - if err then - result = { error = err } + local ok, parsed = pcall(vim.json.decode, res.body) + if ok then + resp = parsed else - result = vim.json.decode(res.body) + resp = res.body end - done = true - end) + result = { error = nil, response = resp } + end + done = true + end) - vim.wait(2000, function() return done end) - return result - ]]) + vim.wait(2000, function() + return done + end) + return result + end) + + return result end describe('vim.net.request', function() @@ -63,40 +50,35 @@ describe('vim.net.request', function() it('fetches a URL into memory (async success)', function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') - local content = exec_lua([[ - local done = false - local result + ---@type table + local result = request('GET', 'https://httpbingo.org/anything') - vim.net.request("https://httpbingo.org/anything", { retry = 3 }, function(err, body) - assert(not err, err) - result = body.body - done = true - end) - - vim.wait(2000, function() return done end) - return result - ]]) - - assert( - content and content:find('"url"%s*:%s*"https://httpbingo.org/anything"'), - 'Expected response body to contain the correct URL' - ) + t.eq(nil, result.error, ('request failed: %s'):format(result.error)) + t.eq('https://httpbingo.org/anything', result.response.url) end) it("detects filetype, sets 'nomodified'", function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') - local rv = exec_lua([[ + local rv = exec_lua(function() vim.cmd('runtime! plugin/nvim/net.lua') vim.cmd('runtime! filetype.lua') -- github raw dump of a small lua file in the neovim repo - vim.cmd('edit https://raw.githubusercontent.com/neovim/neovim/master/runtime/syntax/tutor.lua') - vim.wait(2000, function() return vim.bo.filetype ~= '' end) + vim.cmd( + 'edit https://raw.githubusercontent.com/neovim/neovim/master/runtime/syntax/tutor.lua' + ) + vim.wait(2000, function() + return vim.bo.filetype ~= '' + end) -- wait for buffer to have content - vim.wait(2000, function() return vim.fn.wordcount().bytes > 0 end) - vim.wait(2000, function() return vim.bo.modified == false end) + vim.wait(2000, function() + return vim.fn.wordcount().bytes > 0 + end) + vim.wait(2000, function() + return vim.bo.modified == false + end) return { vim.bo.filetype, vim.bo.modified } - ]]) + end) t.eq('lua', rv[1]) t.eq(false, rv[2], 'Expected buffer to be unmodified for remote content') @@ -104,44 +86,38 @@ describe('vim.net.request', function() it('calls on_response with error on 404 (async failure)', function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') - local err = exec_lua([[ - local done = false - local result - vim.net.request("https://httpbingo.org/status/404", {}, function(e, _) - result = e - done = true - end) - - vim.wait(2000, function() return done end) - return result - ]]) - - assert_404_error(err) + local result = request('GET', 'https://httpbingo.org/status/404') + t.matches('404', result.error) end) it('plugin writes output to buffer', function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') - local content = exec_lua([[ + + local content = exec_lua(function() + ---@type string[] local lines local buf = vim.api.nvim_create_buf(false, true) - vim.net.request("https://httpbingo.org", { outbuf = buf }) + ---@diagnostic disable-next-line: param-type-mismatch + vim.net.request('https://httpbingo.org', { outbuf = buf }) vim.wait(2000, function() lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) - return lines[1] ~= "" + return lines[1] ~= '' end) return lines - ]]) + end) assert(content and content[1]:find('html')) end) it('works with :read', function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') - local content = exec_lua([[ + + local content = exec_lua(function() vim.cmd('runtime plugin/net.lua') + ---@type string[] local lines vim.api.nvim_buf_set_lines(0, 0, -1, false, { 'Here is some text' }) @@ -153,7 +129,8 @@ describe('vim.net.request', function() end) return lines - ]]) + end) + t.eq(true, content ~= nil) t.eq(true, content[1]:find('Here') ~= nil) t.eq(true, content[2]:find('html') ~= nil) @@ -162,11 +139,13 @@ describe('vim.net.request', function() it('opens remote tar.gz URLs as tar archives', function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') - local rv = exec_lua([[ + local rv = exec_lua(function() vim.cmd('runtime! plugin/net.lua') vim.cmd('runtime! plugin/tarPlugin.vim') - vim.cmd('edit https://github.com/neovim/neovim/releases/download/nightly/nvim-macos-x86_64.tar.gz') + vim.cmd( + 'edit https://github.com/neovim/neovim/releases/download/nightly/nvim-macos-x86_64.tar.gz' + ) vim.wait(2500, function() return vim.bo.filetype == 'tar' or vim.b.tarfile ~= nil @@ -177,7 +156,7 @@ describe('vim.net.request', function() modified = vim.bo.modified, tarfile = vim.b.tarfile ~= nil, } - ]]) + end) t.eq('tar', rv.filetype) t.eq(false, rv.modified) @@ -187,7 +166,7 @@ describe('vim.net.request', function() it('opens remote zip URLs as zip archives', function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') - local rv = exec_lua([[ + local rv = exec_lua(function() vim.cmd('runtime! plugin/net.lua') vim.cmd('runtime! plugin/zipPlugin.vim') @@ -202,7 +181,7 @@ describe('vim.net.request', function() modified = vim.bo.modified, zipfile = vim.b.zipfile ~= nil, } - ]]) + end) t.eq('zip', rv.filetype) t.eq(false, rv.modified) @@ -211,79 +190,123 @@ describe('vim.net.request', function() it('accepts custom headers', function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') + ---@type table + local result = request('GET', 'https://httpbingo.org/anything', { + headers = { + Authorization = 'Bearer test-token', + ['X-Custom-Header'] = 'custom-value', + ['Empty'] = '', + }, + }) - local headers = exec_lua([[ - local done = false - local result + t.eq(nil, result.error, ('request failed: %s'):format(result.error)) + t.eq('table', type(result.response.headers), 'Expected headers to be a table') - vim.net.request("https://httpbingo.org/headers", { - headers = { - Authorization = "Bearer test-token", - ['X-Custom-Header'] = "custom-value", - ['Empty'] = '', - }, - }, function(err, res) - if err then - result = { error = err } - else - result = vim.json.decode(res.body).headers - end - done = true - end) - - vim.wait(2000, function() return done end) - return result - ]]) - - t.eq('table', type(headers), 'Expected headers to be a table') -- httpbingo.org/request returns each header as a list in the returned value - t.eq(headers.Authorization[1], 'Bearer test-token', 'Expected Authorization header') - t.eq(headers['X-Custom-Header'][1], 'custom-value', 'Expected X-Custom-Header') - t.eq(headers['Empty'][1], '', 'Expected Empty header') + t.eq( + 'Bearer test-token', + result.response.headers.Authorization[1], + 'Expected Authorization header' + ) + t.eq('custom-value', result.response.headers['X-Custom-Header'][1], 'Expected X-Custom-Header') + t.eq('', result.response.headers['Empty'][1], 'Expected Empty header') end) it('accepts multiple HTTP methods', function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') - t.eq(request('GET', '{}').method, 'GET') - t.eq(request('PUT', '{}').method, 'PUT') - t.eq(request('PATCH', '{}').method, 'PATCH') - t.eq(request('DELETE', '{}').method, 'DELETE') + local url = 'https://httpbingo.org/anything' - ---@diagnostic disable-next-line: no-unknown - local post = - request('POST', "{body='{\"a\": 1}', headers={['Content-Type'] = 'application/json'}}") - t.eq(post.method, 'POST') - t.eq(post.headers['Content-Type'][1], 'application/json') - t.eq(post.json.a, 1) + local function assert_accept_method(method) + local result = request(method, url) + t.eq(nil, result.error) + t.eq(method, result.response.method) + end + + assert_accept_method('GET') + assert_accept_method('PUT') + assert_accept_method('PATCH') + assert_accept_method('DELETE') + + -- HEAD request + local result = request('HEAD', url) + t.eq(nil, result.error) + + -- testing body payload + result = request('POST', url, { + body = '{"a": 1}', + headers = { + ['Content-Type'] = 'application/json', + }, + }) + t.eq(nil, result.error) + t.eq(1, result.response.json.a) end) it('validation', function() - assert_wrong_headers('opts.headers: expected table, got number', '123') - assert_wrong_headers('headers keys and values must be strings', "{ [123] = 'value' }") - assert_wrong_headers('headers keys and values must be strings', '{ Header = 123 }') - assert_wrong_headers( + local function assert_wrong_request(expected_err, method, opts) + if type(method) ~= 'string' then + opts = method + method = 'GET' + end + + local result = t.pcall_err(exec_lua, function() + vim.net.request(method, 'https://example.com', opts) + end) + t.matches(expected_err, result) + end + + -- headers asserts + assert_wrong_request('opts.headers: expected table, got number', { headers = 123 }) + + --- FIXME(ellisonleao): this special assert is failing because the opts table is putting [""] in + --- the key value instead of [123] upon calling the helper method + -- assert_wrong_request( + -- 'headers keys and values must be strings', + -- { headers = { [123] = 'value' } } + -- ) + + assert_wrong_request('headers keys and values must be strings', { headers = { Header = 123 } }) + assert_wrong_request( 'header keys must not start with @ or end with : and ;', - "{ ['Header:'] = 'value' }" + { headers = { ['Header:'] = 'value' } } ) - assert_wrong_headers( + assert_wrong_request( 'header keys must not start with @ or end with : and ;', - "{ ['Header;'] = 'value' }" + { headers = { ['Header;'] = 'value' } } ) - assert_wrong_headers( + assert_wrong_request( 'header keys must not start with @ or end with : and ;', - "{ ['@filename'] = '' }" + { headers = { ['@filename'] = '' } } ) - assert_wrong_body('opts.body: expected body should be string and not start with @', '123') - assert_wrong_body('opts.body: expected body should be string and not start with @', '{}') - assert_wrong_body('opts.body: expected body should be string and not start with @', "'@test'") + -- body asserts + assert_wrong_request( + 'opts.body: expected body should be string and not start with @', + { body = 123 } + ) + assert_wrong_request( + 'opts.body: expected body should be string and not start with @', + { body = {} } + ) + assert_wrong_request( + 'opts.body: expected body should be string and not start with @', + { body = '@test' } + ) -- OPTIONS is not accepted - assert_invalid_method('OPTIONS') - + assert_wrong_request( + 'expected method should be one of GET, POST, PUT, PATCH, HEAD, DELETE, got OPTIONS', + 'OPTIONS' + ) -- lowercase methods are not accepted as well - assert_invalid_method('options') - assert_invalid_method('get') + assert_wrong_request( + 'expected method should be one of GET, POST, PUT, PATCH, HEAD, DELETE, got options', + 'options' + ) + assert_wrong_request( + 'expected method should be one of GET, POST, PUT, PATCH, HEAD, DELETE, got get', + 'get' + ) end) end)