diff --git a/src/devicecode/main.lua b/src/devicecode/main.lua index f6eb27483..7834b213d 100644 --- a/src/devicecode/main.lua +++ b/src/devicecode/main.lua @@ -56,6 +56,51 @@ local function move_to_front(list, wanted) return out end +local function shallow_copy(t) + local out = {} + if type(t) == 'table' then + for k, v in pairs(t) do out[k] = v end + end + return out +end + +local function env_non_empty(name) + local v = os.getenv(name) + if v == nil or v == '' then return nil end + return v +end + +local function ui_opts_with_env_auth(opts) + if type(opts) == 'table' and (opts.auth ~= nil or opts.auth_opts ~= nil) then + return opts + end + + local password = env_non_empty('DEVICECODE_UI_ADMIN_PASSWORD') + if not password then return opts end + + local username = env_non_empty('DEVICECODE_UI_ADMIN_USERNAME') + or env_non_empty('DEVICECODE_UI_ADMIN_USER') + or 'admin' + + local out = shallow_copy(opts) + out.auth_opts = { + users = { + [username] = { + password = password, + principal = { kind = 'user', id = username }, + }, + }, + } + return out +end + +local function service_opts_for(name, opts) + if name == 'ui' then + return ui_opts_with_env_auth(opts) + end + return opts +end + local function cleanup_child_scope(child, reason) if not child then return end child:cancel(reason or 'cleanup') @@ -80,6 +125,8 @@ local function spawn_service(child, bus, name, mod, env, extra_opts) services = extra_opts and extra_opts.services or nil, run_http = extra_opts and extra_opts.run_http or nil, verify_login = extra_opts and extra_opts.verify_login or nil, + auth = extra_opts and extra_opts.auth or nil, + auth_opts = extra_opts and extra_opts.auth_opts or nil, }) error(('service returned unexpectedly: %s'):format(tostring(name)), 0) @@ -222,7 +269,7 @@ function M.run(scope, params) fail_boot(main_conn, name, 'child_scope_failed', cerr) end - local ok_spawn, serr = spawn_service(child, bus, name, mod, env, service_opts[name]) + local ok_spawn, serr = spawn_service(child, bus, name, mod, env, service_opts_for(name, service_opts[name])) if not ok_spawn then cleanup_child_scope(child, 'spawn_failed') fail_boot(main_conn, name, 'spawn_failed', serr) @@ -309,4 +356,8 @@ function M.run(scope, params) end end +M._test = { + service_opts_for = service_opts_for, +} + return M diff --git a/src/services/fabric/hal_transport.lua b/src/services/fabric/hal_transport.lua index e51968f59..f6aa643a4 100644 --- a/src/services/fabric/hal_transport.lua +++ b/src/services/fabric/hal_transport.lua @@ -12,6 +12,7 @@ local op = require 'fibers.op' local protocol = require 'services.fabric.protocol' local resource = require 'devicecode.support.resource' local cap_sdk = require 'services.hal.sdk.cap' +local cap_args = require 'services.hal.types.capability_args' local dep_failure = require 'devicecode.support.dependency_failure' local M = {} @@ -264,6 +265,21 @@ local function reason_text(reason, fallback) return reason or fallback or 'transport_open_failed' end +local function open_opts_for_transport(transport_cfg) + local opts = transport_cfg.open_opts + if transport_cfg.class == 'uart' then + if opts == nil or getmetatable(opts) ~= cap_args.UARTOpenOpts then + local open_opts, err = cap_args.new.UARTOpenOpts(opts) + if not open_opts then + return nil, transport_open_error(transport_cfg, err or 'invalid_uart_open_opts', err) + end + return open_opts, nil + end + end + + return opts or {}, nil +end + local function unwrap_open_transport_reply(transport_cfg, reply, err) -- Backwards-compatible public helper: old callers passed (reply, err). -- New internal callers pass (transport_cfg, reply, err) so structured failures @@ -331,10 +347,14 @@ function M.open_transport_op(conn, transport_cfg, transport_session) transport_cfg.class, transport_cfg.id ) + local open_opts, opts_err = open_opts_for_transport(transport_cfg) + if not open_opts then + return op.always(nil, opts_err) + end return cap:call_control_op( transport_cfg.open_verb or 'open', - transport_cfg.open_opts + open_opts ):wrap(function (reply, err) local session, uerr = unwrap_open_transport_reply(transport_cfg, reply, err) if not session then diff --git a/src/services/fabric/session.lua b/src/services/fabric/session.lua index 8587ff04f..a2ac6ceef 100644 --- a/src/services/fabric/session.lua +++ b/src/services/fabric/session.lua @@ -327,6 +327,23 @@ local function same_peer(cur, frame) and frame.sid == cur.peer_sid end +local function is_self_control_frame(cur, frame) + if type(frame) ~= 'table' or type(frame.sid) ~= 'string' then return false end + if frame.sid ~= cur.local_sid then return false end + if frame.type == 'hello' or frame.type == 'hello_ack' then + return frame.node == nil or frame.node == cur.local_node + end + if frame.type == 'ping' or frame.type == 'pong' then return true end + return false +end + +local function is_unexpected_peer(self, frame) + local expected = self._expected_peer + if expected == nil or expected == '' then return false end + if frame.type ~= 'hello' and frame.type ~= 'hello_ack' then return false end + return frame.node ~= expected +end + local function establish_from_peer(self, frame, at) at = at or fibers.now() local cur = session_snapshot(self) @@ -393,21 +410,15 @@ end local function send_hello(self) local cur = session_snapshot(self) - must_admit_control_frame_now( - self._tx_control, - assert(protocol.hello(cur.local_sid, self._local_node, self._identity_claim, self._auth_claim)), - 'session_hello_send_failed' - ) + local frame = assert(protocol.hello(cur.local_sid, self._local_node, self._identity_claim, self._auth_claim)) + must_admit_control_frame_now(self._tx_control, frame, 'session_hello_send_failed') self._next_hello_at = fibers.now() + self._hello_interval end local function send_hello_ack(self) local cur = session_snapshot(self) - must_admit_control_frame_now( - self._tx_control, - assert(protocol.hello_ack(cur.local_sid, self._local_node, self._identity_claim, self._auth_claim)), - 'session_hello_ack_send_failed' - ) + local frame = assert(protocol.hello_ack(cur.local_sid, self._local_node, self._identity_claim, self._auth_claim)) + must_admit_control_frame_now(self._tx_control, frame, 'session_hello_ack_send_failed') end local function send_ping(self) @@ -554,6 +565,12 @@ end local function handle_session_frame(self, checked, at) local cur = session_snapshot(self) + if is_self_control_frame(cur, checked) then + return + end + if is_unexpected_peer(self, checked) then + return + end if (checked.type == 'hello' or checked.type == 'hello_ack') and not protocol.proto_supported(checked.proto) then @@ -679,6 +696,7 @@ function M.run(scope, params) _transfer_tx = transfer_tx, _session_model = session_model, _local_node = local_node, + _expected_peer = params.peer_id, _identity_claim = protocol.normalise_reserved_claim(params.identity_claim), _auth_claim = protocol.normalise_reserved_claim(params.auth_claim), _auth_state = 'unauthenticated', diff --git a/src/services/hal.lua b/src/services/hal.lua index 33730e032..88a835b07 100644 --- a/src/services/hal.lua +++ b/src/services/hal.lua @@ -113,23 +113,20 @@ end local function device_source_id(device) local meta = device.meta or {} - local candidates = { - meta.source_id, - meta.source, - meta.devpath, - meta.path, - meta.name, - meta.serial, - meta.uid, - } - for i = 1, #candidates do - local v = candidates[i] + local function source_token(v) if type(v) == 'string' and v ~= '' then return path_token(v) end if type(v) == 'number' then return path_token(v) end end - return path_token(('%s_%s'):format(tostring(device.class), tostring(device.id))) + return source_token(meta.source_id) + or source_token(meta.source) + or source_token(meta.devpath) + or source_token(meta.path) + or source_token(meta.name) + or source_token(meta.serial) + or source_token(meta.uid) + or path_token(('%s_%s'):format(tostring(device.class), tostring(device.id))) end ---------------------------------------------------------------------- diff --git a/src/services/hal/managers/uart.lua b/src/services/hal/managers/uart.lua index 82b9e4b5e..ae40e13e7 100644 --- a/src/services/hal/managers/uart.lua +++ b/src/services/hal/managers/uart.lua @@ -63,11 +63,47 @@ local function valid_mode(mode) or mode == '8O1' end -local function validate_config(entries) - if type(entries) ~= 'table' then - return false, 'config must be a list' - end +local function is_sequence(t) + if type(t) ~= 'table' then + return false + end + + local n = 0 + for k in pairs(t) do + if type(k) ~= 'number' or k < 1 or k % 1 ~= 0 then + return false + end + n = n + 1 + end + + return n == #t +end + +local function normalise_config(raw) + if type(raw) ~= 'table' then + return nil, 'config must be a list or table with serial_ports list' + end + + if raw.serial_ports ~= nil then + for k in pairs(raw) do + if k ~= 'serial_ports' then + return nil, 'uart config only supports serial_ports' + end + end + if not is_sequence(raw.serial_ports) then + return nil, 'uart serial_ports must be a list' + end + return raw.serial_ports, nil + end + + if not is_sequence(raw) then + return nil, 'uart config must be a list or table with serial_ports list' + end + return raw, nil +end + +local function validate_config(entries) for _, entry in ipairs(entries) do if type(entry) ~= 'table' then return false, 'each uart entry must be a table' @@ -282,7 +318,12 @@ end function M.apply_config_op(entries) return fibers.run_scope_op(function () - local ok, err = validate_config(entries) + local normalised, norm_err = normalise_config(entries) + if not normalised then + return false, norm_err + end + + local ok, err = validate_config(normalised) if not ok then return false, err end @@ -299,7 +340,7 @@ function M.apply_config_op(entries) local reply_ch = channel.new(1) local admitted, admit_err = fibers.perform(cfg_ch:put_op({ generation = generation, - config = entries, + config = normalised, reply_ch = reply_ch, }):wrap(function () return true, nil diff --git a/src/services/ui/http/request.lua b/src/services/ui/http/request.lua index 7c7597a79..aa4b7c207 100644 --- a/src/services/ui/http/request.lua +++ b/src/services/ui/http/request.lua @@ -21,6 +21,12 @@ if not ok_http_headers then http_headers = nil end local M = {} +local function default_encode_json(value) + local encoded, err = cjson.encode(value) + if encoded == nil then error(err or 'json_encode_failed', 0) end + return encoded +end + local function header_one(headers, name) if not headers then return nil end if http_headers and type(http_headers.get_one) == 'function' then @@ -229,7 +235,7 @@ end function M.run(scope, ctx, deps) deps = deps or {} - local owner = response_mod.new(ctx, { encode = deps.encode_json }) + local owner = response_mod.new(ctx, { encode = deps.encode_json or default_encode_json }) scope:finally(function (_, status, primary) resource.terminate_checked(owner, primary or status or 'request_closed', 'HTTP response termination') diff --git a/tests/unit/fabric/test_link.lua b/tests/unit/fabric/test_link.lua index 55aff4a0c..dc514b6e9 100644 --- a/tests/unit/fabric/test_link.lua +++ b/tests/unit/fabric/test_link.lua @@ -77,7 +77,7 @@ function tests.test_session_control_establishes_from_hello_and_sends_ack() local ok, err = scope:spawn(function () local result = session.run(scope, { link_id = 'link-session', - peer_id = 'peer-a', + peer_id = 'peer-node', local_node = 'local-a', local_sid = 'local-sid', frame_rx = control_rx, @@ -133,7 +133,7 @@ function tests.test_session_liveness_timeout_resets_to_hello() local ok, err = scope:spawn(function () local result = session.run(scope, { link_id = 'link-liveness', - peer_id = 'peer-a', + peer_id = 'peer-node', local_sid = 'local-sid', frame_rx = control_rx, tx_control = out_tx, @@ -198,7 +198,7 @@ function tests.test_session_ping_is_emitted_before_liveness_deadline() local ok, err = scope:spawn(function () local result = session.run(scope, { link_id = 'link-ping', - peer_id = 'peer-a', + peer_id = 'peer-node', local_sid = 'local-sid', frame_rx = control_rx, tx_control = out_tx, @@ -247,7 +247,7 @@ function tests.test_session_control_processes_ready_control_before_timer_work() local ok, err = scope:spawn(function () local result = session.run(scope, { link_id = 'link-control-before-timer', - peer_id = 'peer-a', + peer_id = 'peer-node', local_sid = 'local-sid', frame_rx = control_rx, tx_control = out_tx, diff --git a/tests/unit/fabric/test_session.lua b/tests/unit/fabric/test_session.lua index 9d9f85cbc..e5ef51453 100644 --- a/tests/unit/fabric/test_session.lua +++ b/tests/unit/fabric/test_session.lua @@ -45,6 +45,13 @@ local function recv_with_timeout(rx, label, timeout) return item end +local function expect_no_item(rx, label, timeout) + timeout = timeout or 0.05 + fibers.perform(sleep.sleep_op(timeout)) + local item = queue.try_recv_now(rx) + assert_nil(item, label or 'expected no queued item') +end + local function start_session(scope, opts) opts = opts or {} local frame_tx, frame_rx = mailbox.new(16, { full = 'reject_newest' }) @@ -57,7 +64,7 @@ local function start_session(scope, opts) local ok, err = scope:spawn(function () local result = session.run(scope, { link_id = opts.link_id or 'link-a', - peer_id = opts.peer_id or 'peer-a', + peer_id = opts.peer_id or 'mcu', local_node = opts.local_node or 'cm5', local_sid = opts.local_sid or 'cm5-sid', identity_claim = opts.identity_claim, @@ -213,7 +220,61 @@ function tests.test_new_peer_sid_drops_old_generation_and_starts_next_generation end) end +function tests.test_session_ignores_self_echoed_hello_before_expected_peer() + fibers.run(function (scope) + local h = start_session(scope) + + local hello = recv_with_timeout(h.control_rx, 'initial hello') + assert_eq(hello.frame.type, 'hello') + assert_eq(hello.frame.sid, 'cm5-sid') + assert_eq(hello.frame.node, 'cm5') + + admit_frame(h.frame_tx, assert(protocol.hello('cm5-sid', 'cm5'))) + expect_no_item(h.rpc_rx, 'self echo should not emit rpc peer session') + expect_no_item(h.transfer_rx, 'self echo should not emit transfer peer session') + expect_no_item(h.control_rx, 'self echo should not trigger hello_ack') + admit_frame(h.frame_tx, assert(protocol.hello_ack('mcu-sid', 'mcu'))) + local rpc_ev = recv_with_timeout(h.rpc_rx, 'rpc peer session after real ack') + local xfer_ev = recv_with_timeout(h.transfer_rx, 'transfer peer session after real ack') + assert_eq(rpc_ev.kind, 'peer_session') + assert_eq(xfer_ev.kind, 'peer_session') + assert_eq(rpc_ev.session.peer_node, 'mcu') + assert_eq(rpc_ev.session.peer_sid, 'mcu-sid') + assert_eq(xfer_ev.session.peer_node, 'mcu') + assert_eq(xfer_ev.session.peer_sid, 'mcu-sid') + + h.frame_tx:close('done') + recv_with_timeout(h.done_rx, 'session done') + end) +end + +function tests.test_session_rejects_wrong_peer_handshake_before_expected_peer() + fibers.run(function (scope) + local h = start_session(scope, { peer_id = 'mcu' }) + recv_with_timeout(h.control_rx, 'initial hello') + + admit_frame(h.frame_tx, assert(protocol.hello_ack('wrong-sid', 'bigbox-cm5'))) + expect_no_item(h.rpc_rx, 'wrong peer ack should not emit rpc peer session') + expect_no_item(h.transfer_rx, 'wrong peer ack should not emit transfer peer session') + + admit_frame(h.frame_tx, assert(protocol.hello('mcu-sid', 'mcu'))) + local rpc_ev = recv_with_timeout(h.rpc_rx, 'rpc peer session after real hello') + local xfer_ev = recv_with_timeout(h.transfer_rx, 'transfer peer session after real hello') + assert_eq(rpc_ev.kind, 'peer_session') + assert_eq(xfer_ev.kind, 'peer_session') + assert_eq(rpc_ev.session.peer_node, 'mcu') + assert_eq(rpc_ev.session.peer_sid, 'mcu-sid') + assert_eq(xfer_ev.session.peer_node, 'mcu') + assert_eq(xfer_ev.session.peer_sid, 'mcu-sid') + + local ack = recv_with_timeout(h.control_rx, 'hello ack for real peer') + assert_eq(ack.frame.type, 'hello_ack') + + h.frame_tx:close('done') + recv_with_timeout(h.done_rx, 'session done') + end) +end function tests.test_wire_errors_below_limit_are_counted_without_dropping_session() fibers.run(function (scope) diff --git a/tests/unit/hal/service_raw_host_spec.lua b/tests/unit/hal/service_raw_host_spec.lua index 1b7137c5c..988916b17 100644 --- a/tests/unit/hal/service_raw_host_spec.lua +++ b/tests/unit/hal/service_raw_host_spec.lua @@ -6,6 +6,7 @@ local op = require 'fibers.op' local probe = require 'tests.support.bus_probe' local runfibers = require 'tests.support.run_fibers' +local pty = require 'tests.support.pty' local cap_sdk = require 'services.hal.sdk.cap' local hal_types = require 'services.hal.types.core' @@ -524,4 +525,50 @@ function T.hal_keeps_legacy_public_capability_topics_for_compatibility() end) end +function T.hal_config_uart_serial_ports_registers_uart_capability() + runfibers.run(function(scope) + local fs_manager = new_bootstrap_filesystem_manager() + local port = pty.open(scope) + + with_real_hal(scope, { + ['services.hal.managers.filesystem'] = fs_manager, + }, function(bus) + local reader = bus:connect() + local admin = bus:connect() + + publish_hal_config(admin, { + schema = 'devicecode.config/hal/1', + uart = { + serial_ports = { + { + id = 'uart0', + path = port.slave_name, + baud = 115200, + mode = '8N1', + }, + }, + }, + }) + + local device_meta = wait_payload(reader, { 'dev', 'uart', 'uart0', 'meta' }, 0.5) + assert(type(device_meta) == 'table') + assert(device_meta.source == 'uart_manager', 'device_source=' .. tostring(device_meta.source)) + + local public_status = wait_payload(reader, { 'cap', 'uart', 'uart0', 'status' }, 0.5) + assert(type(public_status) == 'table') + assert(public_status.state == 'available') + assert(public_status.available == true) + assert(public_status.source_kind == 'host') + assert(public_status.source == 'uart_manager', 'source=' .. tostring(public_status.source)) + + local raw_status = wait_payload(reader, { + 'raw', 'host', 'uart_manager', 'cap', 'uart', 'uart0', 'status' + }, 0.5) + assert(type(raw_status) == 'table') + assert(raw_status.state == 'available') + assert(raw_status.available == true) + end) + end) +end + return T diff --git a/tests/unit/hal/uart_manager_spec.lua b/tests/unit/hal/uart_manager_spec.lua index 012864b18..13ccc4918 100644 --- a/tests/unit/hal/uart_manager_spec.lua +++ b/tests/unit/hal/uart_manager_spec.lua @@ -74,6 +74,81 @@ function T.apply_config_op_adds_uart_driver_and_emits_added_event() end) end +function T.apply_config_op_accepts_serial_ports_wrapper_from_hal_config() + local M = fresh_manager() + + runfibers.run(function(scope) + local port = pty.open(scope) + local dev_ev_ch = channel.new(8) + local cap_emit_ch = channel.new(16) + + local ok_start, err_start = fibers.perform(M.start_op(nil, dev_ev_ch, cap_emit_ch)) + assert(ok_start == true, tostring(err_start)) + + local ok_cfg, err_cfg = fibers.perform(M.apply_config_op({ + serial_ports = { + { id = 'uart0', path = port.slave_name, baud = 115200, mode = '8N1' }, + }, + })) + assert(ok_cfg == true, tostring(err_cfg)) + + local ev = recv_or_fail(dev_ev_ch) + assert(ev.event_type == 'added') + assert(ev.class == 'uart') + assert(ev.id == 'uart0') + assert(type(ev.capabilities) == 'table' and #ev.capabilities == 1) + assert(ev.capabilities[1].class == 'uart') + assert(ev.meta.path == port.slave_name) + + local ok_stop, err_stop = fibers.perform(M.shutdown_op()) + assert(ok_stop == true, tostring(err_stop)) + end) +end + +function T.apply_config_op_rejects_invalid_wrapped_config() + local M = fresh_manager() + + runfibers.run(function() + local dev_ev_ch = channel.new(8) + local cap_emit_ch = channel.new(16) + + local ok_start, err_start = fibers.perform(M.start_op(nil, dev_ev_ch, cap_emit_ch)) + assert(ok_start == true, tostring(err_start)) + + local ok_cfg, err_cfg = fibers.perform(M.apply_config_op({ + serial_ports = 'bad', + })) + assert(ok_cfg == false) + assert(tostring(err_cfg):match('serial_ports must be a list')) + + local ok_stop, err_stop = fibers.perform(M.shutdown_op()) + assert(ok_stop == true, tostring(err_stop)) + end) +end + +function T.apply_config_op_rejects_non_list_table_without_serial_ports() + local M = fresh_manager() + + runfibers.run(function() + local dev_ev_ch = channel.new(8) + local cap_emit_ch = channel.new(16) + + local ok_start, err_start = fibers.perform(M.start_op(nil, dev_ev_ch, cap_emit_ch)) + assert(ok_start == true, tostring(err_start)) + + local ok_cfg, err_cfg = fibers.perform(M.apply_config_op({ + ports = { + { id = 'uart0', path = '/dev/ttyS0' }, + }, + })) + assert(ok_cfg == false) + assert(tostring(err_cfg):match('serial_ports list')) + + local ok_stop, err_stop = fibers.perform(M.shutdown_op()) + assert(ok_stop == true, tostring(err_stop)) + end) +end + function T.reapply_same_config_is_idempotent() local M = fresh_manager() diff --git a/tests/unit/main/service_spec.lua b/tests/unit/main/service_spec.lua index a0bd146e5..9ec905db4 100644 --- a/tests/unit/main/service_spec.lua +++ b/tests/unit/main/service_spec.lua @@ -3,11 +3,39 @@ local mainmod = require 'devicecode.main' local runfibers = require 'tests.support.run_fibers' local busmod = require 'bus' +local ui_auth = require 'services.ui.auth' local safe = require 'coxpcall' +local stdlib = require 'posix.stdlib' local T = {} +local function setenv(name, value) + assert(stdlib.setenv(name, value or '', true)) +end + +local function with_env(values, fn) + local names = {} + local old = {} + for name, _ in pairs(values) do + names[#names + 1] = name + old[name] = os.getenv(name) + end + + for name, value in pairs(values) do + setenv(name, value) + end + + local ok, err = xpcall(fn, debug.traceback) + + for i = 1, #names do + local name = names[i] + setenv(name, old[name] or '') + end + + if not ok then error(err, 0) end +end + function T.main_rejects_duplicate_service_names() local ok, err = safe.pcall(function() runfibers.run(function(scope) @@ -44,4 +72,60 @@ function T.main_fails_boot_when_service_load_fails() assert(tostring(err):match('boot failed')) end +function T.main_builds_ui_admin_auth_opts_from_env() + with_env({ + DEVICECODE_UI_ADMIN_PASSWORD = 'e2e', + DEVICECODE_UI_ADMIN_USERNAME = '', + DEVICECODE_UI_ADMIN_USER = '', + }, function() + local opts = mainmod._test.service_opts_for('ui', nil) + assert(opts.auth_opts.users.admin.password == 'e2e') + assert(opts.auth_opts.users.admin.principal.kind == 'user') + assert(opts.auth_opts.users.admin.principal.id == 'admin') + + local verifier = ui_auth.new(opts.auth_opts) + local principal, err = verifier:verify({ username = 'admin', password = 'e2e' }) + assert(principal, tostring(err)) + assert(principal.kind == 'user') + assert(principal.id == 'admin') + end) +end + +function T.main_uses_custom_ui_admin_username_from_env() + with_env({ + DEVICECODE_UI_ADMIN_PASSWORD = 'secret', + DEVICECODE_UI_ADMIN_USERNAME = 'tester', + DEVICECODE_UI_ADMIN_USER = '', + }, function() + local opts = mainmod._test.service_opts_for('ui', nil) + assert(opts.auth_opts.users.tester.password == 'secret') + + local verifier = ui_auth.new(opts.auth_opts) + local principal, err = verifier:verify({ username = 'tester', password = 'secret' }) + assert(principal, tostring(err)) + assert(principal.id == 'tester') + end) +end + +function T.main_does_not_override_explicit_ui_auth_opts() + local explicit = { + auth_opts = { + users = { + tester = { password = 'test-password', principal = { kind = 'user', id = 'tester' } }, + }, + }, + } + + with_env({ + DEVICECODE_UI_ADMIN_PASSWORD = 'e2e', + DEVICECODE_UI_ADMIN_USERNAME = '', + DEVICECODE_UI_ADMIN_USER = '', + }, function() + local opts = mainmod._test.service_opts_for('ui', explicit) + assert(opts == explicit) + assert(opts.auth_opts.users.tester.password == 'test-password') + assert(opts.auth_opts.users.admin == nil) + end) +end + return T diff --git a/tests/unit/ui/test_http_request.lua b/tests/unit/ui/test_http_request.lua index 92b4afb53..8174b8e61 100644 --- a/tests/unit/ui/test_http_request.lua +++ b/tests/unit/ui/test_http_request.lua @@ -6,6 +6,8 @@ local channel = require 'fibers.channel' local busmod = require 'bus' local request = require 'services.ui.http.request' local read_model = require 'services.ui.read_model' +local ui_auth = require 'services.ui.auth' +local sessions = require 'services.ui.sessions' local ok_cjson, cjson = pcall(require, 'cjson.safe') if not ok_cjson then cjson = require 'cjson' end @@ -103,6 +105,34 @@ function tests.test_http_response_writer_may_yield_inside_request_scope_without_ end) end +function tests.test_login_uses_default_json_encoder_when_not_injected() + run_fibers.run(function (scope) + local ctx = fake_ctx('POST', '/api/login') + ctx.headers = { ['content-type'] = 'application/json' } + ctx.read_body_as_string_op = function () + return fibers.always('{"username":"admin","password":"e2e"}', nil) + end + + local result = request.run(scope, ctx, { + auth = ui_auth.new({ + users = { + admin = { password = 'e2e', principal = { kind = 'user', id = 'admin' } }, + }, + }), + sessions = sessions.new(), + }) + + assert_eq(result.status, 'ok') + assert_eq(#ctx.replies, 1) + assert_eq(ctx.replies[1].status, 200) + + local decoded, derr = cjson.decode(ctx.replies[1].body) + assert_not_nil(decoded, derr) + assert_not_nil(decoded.session, ctx.replies[1].body) + assert_not_nil(decoded.session.id, ctx.replies[1].body) + end) +end + function tests.test_http_command_route_parses_real_json_body_and_calls_bus() run_fibers.run(function (scope)