Skip to content

How to mock UCI configuration

Declare uci: null in your utest.config.uc mocks table before using any of the patterns below. See How-to: Mock a module with mock.inject() for the setup steps.


Seed a configuration package

The data map uses package names as top-level keys. Each package value is an object of section names, where each section is an object of option keys including the mandatory .type key:

const uci_data = {
    'luci-sso': {
        'default': {
            '.type': 'oidc',
            'enabled': '1',
            'issuer_url': 'https://idp.example.com',
            'client_id': 'my-client'
        },
        'admin_role': {
            '.type': 'role',
            'email': ['admin@example.com'],
            'read': ['*'],
            'write': ['*']
        }
    }
};

mock.inject('uci', { data: uci_data }, (m_uci) => {
    let c = m_uci.cursor();
    assert.match('1', c.get('luci-sso', 'default', 'enabled'));
    assert.match(null, c.get('luci-sso', 'default', 'missing_opt'));
    assert.match(null, c.get('no-pkg', 'sec', 'opt'));
});

Read an entire section at once

get_all(pkg, section) returns the full section object including .type and .name (the section name), or null if the section does not exist:

mock.inject('uci', { data: {
    'luci-sso': {
        'default': { '.type': 'oidc', 'issuer_url': 'https://idp.example.com', 'client_id': 'my-client' }
    }
}}, (m_uci) => {
    let c = m_uci.cursor();
    let sec = c.get_all('luci-sso', 'default');
    assert.match('default',   sec['.name']);
    assert.match('oidc',      sec['.type']);
    assert.match('my-client', sec.client_id);
    assert.match(null, c.get_all('luci-sso', 'no-sec'));
});

Iterate sections by type

foreach(pkg, type, callback) iterates sections whose .type matches the given string, in insertion order. The callback receives each section with .name injected:

mock.inject('uci', { data: {
    'luci-sso': {
        'admin_role': { '.type': 'role', 'email': ['admin@example.com'] }
    }
}}, (m_uci) => {
    let names = [];
    m_uci.cursor().foreach('luci-sso', 'role', (s) => push(names, s['.name']));
    assert.match(['admin_role'], names);
});

Write options and verify they persist within the mock

set(pkg, section, option, value) writes a value and makes it immediately readable via get(). The change is confined to the mock layer and does not touch the real UCI database:

mock.inject('uci', { data: {
    'luci-sso': { 'default': { '.type': 'oidc', 'enabled': '0' } }
}}, (m_uci) => {
    let c = m_uci.cursor();
    c.set('luci-sso', 'default', 'enabled', '1');
    assert.match('1', c.get('luci-sso', 'default', 'enabled'));
});

Delete an option or an entire section

Pass three arguments to delete a single option; pass two to delete the entire section:

// Delete one option
mock.inject('uci', { data: {
    'luci-sso': { 'default': { '.type': 'oidc', 'enabled': '1', 'scope': 'openid' } }
}}, (m_uci) => {
    let c = m_uci.cursor();
    c.delete('luci-sso', 'default', 'scope');
    assert.match(null, c.get('luci-sso', 'default', 'scope'));
    assert.match('1',  c.get('luci-sso', 'default', 'enabled'));
});

// Delete an entire section
mock.inject('uci', { data: {
    'luci-sso': {
        'admin_role': { '.type': 'role', 'email': ['admin@example.com'] }
    }
}}, (m_uci) => {
    let c = m_uci.cursor();
    c.delete('luci-sso', 'admin_role');
    let names = [];
    c.foreach('luci-sso', 'role', (s) => push(names, s['.name']));
    assert.match([], names);
});

Exercise code paths that call load()

load() is a no-op that returns true. Call it in tests that exercise code paths which explicitly load a package before reading:

mock.inject('uci', { data: uci_data }, (m_uci) => {
    assert.match(truthy(), m_uci.cursor().load('luci-sso'));
});

If you need to verify that load was called, inspect the cursor's call log with spy() (call it on the cursor, not the top-level uci proxy):

import { spy } from 'utest';

mock.inject('uci', { data: uci_data }, (m_uci) => {
    let c = m_uci.cursor();
    c.load('luci-sso');
    assert.match([['luci-sso']], spy(c).calls.load);
});

Exercise code paths that call commit()

commit() is a no-op that returns true. Call it in tests that exercise code paths which persist configuration:

mock.inject('uci', {}, (m_uci) => {
    assert.match(true, m_uci.cursor().commit('luci-sso'));
});

Next steps