[SSSD] [PATCH] RFC: Unit test for the NSS responder based on the cmocka library

Jakub Hrozek jhrozek at redhat.com
Tue Feb 12 17:12:13 UTC 2013


Hi,

Short version - attached is a patch proposal for a unit test based on
cmocka. The patches are an example of a complex, yet isolated unit test
and I'd like to get opinions on whether this would be a good way to go.

Long version:
Our check based test suite is OK for developing simple tests for APIs
like sysdb but not really great for testing of more complex interfaces.

In particular, it's hard to simulate certain pieces of functionality
that require interaction with network or the rest of the system such as
LDAP searches or logins, especially in environments like buildsystems.

I think we might want to look at Cmocka to provide this missing functionality
and test complex interfaces in isolation. Cmocka[1] is a fork of Google's
cmockery which is not maintained upstream anymore. Cmocka is maintained
by Andreas Schneider.

I've been playing with cmocka lately and wrote a couple of patches that
add a unit test for the NSS responder, in particular the getpwnam()
function. It was quite easy to simulate the Data Provider and the client
using two concepts:

1) cmocka's mocking feature
Functions that provide external interface, like the dummy DP functions
in my patches, can access a stack of values. The unit test can push some
expected return values onto the stack before launching the test and the
dummy functions would then pop the values and use them as appropriate.
It's quite easy to simulate a Data Provider saving an entry to cache
with call like this (pseudocode):

int unit_test()
{
    will_return(sss_dp_get_account_recv, "testuser");
    will_return(sss_dp_get_account_recv, 10);
    will_return(sss_dp_get_account_recv, 11);
}

int sss_dp_get_account_recv(TALLOC_CTX *mem_ctx, tevent_req *req)
{
    char *username = mock();
    uid_t uid = mock();
    gid_t gid = mock();

    sysdb_store_user(username, uid, gid);
}

2) the -wrap feature of ld
It is possible to selectively override functions from modules linked against
to intercept calls using the -wrap feature of ld. In my unit tests I used
this feature to check results of responder search with defined callbacks
instead of returning the results to the nss client. Here is a simple example
of intercepting a call to negative cache to make sure the negative cache
is hit when it's supposed to:

int __wrap_sss_ncache_check_user(struct sss_nc_ctx *ctx, int ttl,
                                 struct sss_domain_info *dom,
                                 const char *name)
{
    int ret;

    ret = __real_sss_ncache_check_user(ctx, ttl, dom, name);
    if (ret == EEXIST) {
        nss_test_ctx->ncache_hit = true;
    }
    return ret;
}

There's a couple of boilerplate functions but once these are in place,
they can be reused to cover different pieces of functionality..the
attached patches test the following requests:
 * cached user including assertion that DP was not contacted
 * nonexistant user including assertion that on a subsequent lookup, the
   request is returned from negative cache
 * search for a user that has not been cache prior to the search
 * updating an expired user including assertions that the user had been
   updated from DP and the updated entry is returned.

The downsides of using cmocka as far as I can see are:
  * writing the mocking code can be tedious. On the other hand, the NSS
    responder I picked was probably the most complex code I could test
    as it communicates with both DP and the client library. Even more
    isolated testing (the AD sites feature comes to mind) would be much
    easier I think.
  * cmocka is not present in RHEL

I'll be glad to hear comments about this unit test proposal. Patches
are attached.

[1] http://cmocka.cryptomilk.org/
-------------- next part --------------
>From 53e93263d939c47c3d52f6da965199c5bcab9e72 Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhrozek at redhat.com>
Date: Tue, 12 Feb 2013 14:47:35 +0100
Subject: [PATCH 1/4] Detect the presence of libcmocka during configure

---
 configure.ac              |  2 ++
 src/external/libcmocka.m4 | 19 +++++++++++++++++++
 2 files changed, 21 insertions(+)
 create mode 100644 src/external/libcmocka.m4

diff --git a/configure.ac b/configure.ac
index af976151ad3c90bfcb056dd7d8d24bbc8b884ca5..0e7712389d99cf1b903ae1dbf063d889be62d7b0 100644
--- a/configure.ac
+++ b/configure.ac
@@ -137,6 +137,7 @@ m4_include([src/external/ldap.m4])
 m4_include([src/external/libpcre.m4])
 m4_include([src/external/krb5.m4])
 m4_include([src/external/libcares.m4])
+m4_include([src/external/libcmocka.m4])
 m4_include([src/external/docbook.m4])
 m4_include([src/external/sizes.m4])
 m4_include([src/external/python.m4])
@@ -269,6 +270,7 @@ AC_PATH_PROG([DOXYGEN], [doxygen], [false])
 AM_CONDITIONAL([HAVE_DOXYGEN], [test x$DOXYGEN != xfalse ])
 
 AM_CONDITIONAL([HAVE_CHECK], [test x$have_check != x])
+AM_CONDITIONAL([HAVE_CMOCKA], [test x$have_cmocka != x])
 
 abs_build_dir=`pwd`
 AC_DEFINE_UNQUOTED([ABS_BUILD_DIR], ["$abs_build_dir"], [Absolute path to the build directory])
diff --git a/src/external/libcmocka.m4 b/src/external/libcmocka.m4
new file mode 100644
index 0000000000000000000000000000000000000000..c57327612e2eec2411f18b640c4e2eaed2b31bc1
--- /dev/null
+++ b/src/external/libcmocka.m4
@@ -0,0 +1,19 @@
+dnl this file will be simplified when cmocka carries a .pc file
+AC_SUBST(CMOCKA_LIBS)
+AC_SUBST(CMOCKA_CFLAGS)
+
+AC_CHECK_HEADERS(
+    [setjmp.h cmocka.h],
+    [AC_CHECK_LIB([cmocka], [_will_return],
+                  [ CMOCKA_LIBS="-lcmocka"
+                    have_cmocka="yes" ],
+                  [AC_MSG_WARN([No libcmocka library found])
+                    have_cmocka="no" ])],
+    [AC_MSG_WARN([libcmocka header files not installed])],
+    [[ #include <stdarg.h>
+     # include <stddef.h>
+     #ifdef HAVE_SETJMP_H
+     # include <setjmp.h>
+     #endif
+    ]]
+)
-- 
1.8.1.2

-------------- next part --------------
>From 4c038e3bca18a0021edc0dd4506d5c709d80caa5 Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhrozek at redhat.com>
Date: Tue, 12 Feb 2013 13:18:00 +0100
Subject: [PATCH 2/4] Add utility functions for tests

---
 src/tests/common.h     |  37 +++++++++
 src/tests/common_dom.c | 199 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 236 insertions(+)
 create mode 100644 src/tests/common_dom.c

diff --git a/src/tests/common.h b/src/tests/common.h
index f13181b751fef517f609c6208977cc82a825014e..0c663eb457877306dd048046692a344d93f65415 100644
--- a/src/tests/common.h
+++ b/src/tests/common.h
@@ -47,6 +47,11 @@ void leak_check_teardown(void);
 
 void tests_set_cwd(void);
 
+struct tevent_req *mock_request_send(TALLOC_CTX *mem_ctx, struct tevent_context *ev);
+
+errno_t mock_request_recv(struct tevent_req *req);
+
+
 errno_t
 compare_dp_options(struct dp_option *map1, size_t size1,
                    struct dp_option *map2);
@@ -55,4 +60,36 @@ errno_t
 compare_sdap_attr_maps(struct sdap_attr_map *map1, size_t size1,
                        struct sdap_attr_map *map2);
 
+/* A common test structure for tests that require a domain to be set up. */
+struct sss_test_ctx {
+    struct sysdb_ctx *sysdb;
+    struct confdb_ctx *confdb;
+    struct tevent_context *ev;
+    struct sss_domain_info *dom;
+
+    bool done;
+    int error;
+};
+
+struct sss_test_conf_param {
+    const char *key;
+    const char *value;
+};
+
+struct sss_test_ctx *
+create_dom_test_ctx(TALLOC_CTX *mem_ctx,
+                    const char *tests_path,
+                    const char *confdb_path,
+                    const char *sysdb_path,
+                    const char *domain_name,
+                    const char *id_provider,
+                    struct sss_test_conf_param *params);
+
+void test_dom_suite_setup(const char *tests_path);
+
+void test_dom_suite_cleanup(const char *tests_path,
+                            const char *confdb_path,
+                            const char *sysdb_path);
+
+int test_dom_loop(struct sss_test_ctx *tctx);
 #endif /* !__TESTS_COMMON_H__ */
diff --git a/src/tests/common_dom.c b/src/tests/common_dom.c
new file mode 100644
index 0000000000000000000000000000000000000000..a55072c34da6aa31fd28ed83ac99aa5078611add
--- /dev/null
+++ b/src/tests/common_dom.c
@@ -0,0 +1,199 @@
+/*
+    Authors:
+        Jakub Hrozek <jhrozek at redhat.com>
+
+    Copyright (C) 2013 Red Hat
+
+    SSSD tests: Common utilities for tests that exercise domains
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <talloc.h>
+#include <tevent.h>
+#include <errno.h>
+
+#include "tests/common.h"
+#include "util/util.h"
+
+struct tevent_req *
+mock_request_send(TALLOC_CTX *mem_ctx, struct tevent_context *ev)
+{
+    struct tevent_req *req;
+    int state;
+
+    req = tevent_req_create(mem_ctx, &state, int);
+    if (!req) return NULL;
+
+    tevent_req_done(req);
+    tevent_req_post(req, ev);
+    return req;
+}
+
+errno_t mock_request_recv(struct tevent_req *req)
+{
+    TEVENT_REQ_RETURN_ON_ERROR(req);
+
+    return EOK;
+}
+
+struct sss_test_ctx *
+create_dom_test_ctx(TALLOC_CTX *mem_ctx,
+                    const char *tests_path,
+                    const char *confdb_path,
+                    const char *sysdb_path,
+                    const char *domain_name,
+                    const char *id_provider,
+                    struct sss_test_conf_param *params)
+{
+    struct sss_test_ctx *test_ctx;
+    char *conf_db;
+    size_t i;
+    const char *val[2];
+    val[1] = NULL;
+    errno_t ret;
+    char *dompath;
+
+    test_ctx = talloc_zero(mem_ctx, struct sss_test_ctx);
+    if (test_ctx == NULL) {
+        DEBUG(SSSDBG_CRIT_FAILURE, ("talloc_zero failed\n"));
+        goto fail;
+    }
+
+    /* Create an event context */
+    test_ctx->ev = tevent_context_init(test_ctx);
+    if (test_ctx->ev == NULL) {
+        DEBUG(SSSDBG_CRIT_FAILURE, ("tevent_context_init failed\n"));
+        goto fail;
+    }
+
+    conf_db = talloc_asprintf(test_ctx, "%s/%s", tests_path, confdb_path);
+    if (conf_db == NULL) {
+        DEBUG(SSSDBG_CRIT_FAILURE, ("talloc_asprintf failed\n"));
+        goto fail;
+    }
+
+    /* Connect to the conf db */
+    ret = confdb_init(test_ctx, &test_ctx->confdb, conf_db);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_CRIT_FAILURE, ("confdb_init failed: %d\n", ret));
+        goto fail;
+    }
+
+    val[0] = domain_name;
+    ret = confdb_add_param(test_ctx->confdb, true,
+                           "config/sssd", "domains", val);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_CRIT_FAILURE, ("cannot add domain: %d\n", ret));
+        goto fail;
+    }
+
+    dompath = talloc_asprintf(test_ctx, "config/domain/%s", domain_name);
+    if (dompath == NULL) {
+        DEBUG(SSSDBG_CRIT_FAILURE, ("talloc_asprintf failed\n"));
+        goto fail;
+    }
+
+    val[0] = id_provider;
+    ret = confdb_add_param(test_ctx->confdb, true,
+                           dompath, "id_provider", val);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_CRIT_FAILURE, ("cannot add id_provider: %d\n", ret));
+        goto fail;
+    }
+
+    if (params) {
+        for (i=0; params[i].key; i++) {
+            val[0] = params[i].key;
+            ret = confdb_add_param(test_ctx->confdb, true,
+                                   dompath, params[i].value, val);
+            if (ret != EOK) {
+                DEBUG(SSSDBG_CRIT_FAILURE,
+                      ("cannot add parameter %s: %d\n", params[i].key, ret));
+                goto fail;
+            }
+        }
+    }
+
+    ret = sssd_domain_init(test_ctx, test_ctx->confdb, domain_name,
+                           tests_path, &test_ctx->dom);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_CRIT_FAILURE, ("cannot add id_provider: %d\n", ret));
+        goto fail;
+    }
+    test_ctx->sysdb = test_ctx->dom->sysdb;
+
+    return test_ctx;
+
+fail:
+    talloc_free(test_ctx);
+    return NULL;
+}
+
+void test_dom_suite_setup(const char *tests_path)
+{
+    errno_t ret;
+
+    /* Create tests directory if it doesn't exist */
+    /* (relative to current dir) */
+    ret = mkdir(tests_path, 0775);
+    if (ret != 0 && errno != EEXIST) {
+        DEBUG(SSSDBG_CRIT_FAILURE,
+              ("Could not create test directory\n"));
+    }
+}
+
+void test_dom_suite_cleanup(const char *tests_path,
+                            const char *confdb_path,
+                            const char *sysdb_path)
+{
+    errno_t ret;
+    char *conf_db;
+    char *sys_db;
+
+    conf_db = talloc_asprintf(NULL, "%s/%s", tests_path, confdb_path);
+    sys_db = talloc_asprintf(NULL, "%s/%s", tests_path, sysdb_path);
+    if (!conf_db || !sys_db) {
+        DEBUG(SSSDBG_CRIT_FAILURE,
+              ("Could not construct db paths\n"));
+    }
+
+    errno = 0;
+    ret = unlink(conf_db);
+    if (ret != 0 && errno != ENOENT) {
+        DEBUG(SSSDBG_CRIT_FAILURE,
+              ("Could not delete the test config ldb file (%d) (%s)\n",
+               errno, strerror(errno)));
+    }
+
+    errno = 0;
+    ret = unlink(sys_db);
+    if (ret != 0 && errno != ENOENT) {
+        DEBUG(SSSDBG_CRIT_FAILURE,
+              ("Could not delete the test ldb file (%d) (%s)\n",
+               errno, strerror(errno)));
+    }
+
+    talloc_free(conf_db);
+    talloc_free(sys_db);
+}
+
+int test_dom_loop(struct sss_test_ctx *tctx)
+{
+    while (!tctx->done)
+        tevent_loop_once(tctx->ev);
+
+    return tctx->error;
+}
+
-- 
1.8.1.2

-------------- next part --------------
>From 3449f562443508a5a9f7e62134dbf60dc1a1d072 Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhrozek at redhat.com>
Date: Tue, 12 Feb 2013 13:58:46 +0100
Subject: [PATCH 3/4] Move sss_cmd_execute from client to responder code.

I think it logically belongs there and allows to better exercise the
responder commands from unit tests.
---
 src/responder/common/responder.h        |  4 +++-
 src/responder/common/responder_cmd.c    |  8 +++-----
 src/responder/common/responder_common.c | 10 +++++++++-
 3 files changed, 15 insertions(+), 7 deletions(-)

diff --git a/src/responder/common/responder.h b/src/responder/common/responder.h
index a265d61d920d6f649f99aed0d30c17866c3fa4a4..110bd38d317dbacb10b636a6b56cdae8b7a1af9b 100644
--- a/src/responder/common/responder.h
+++ b/src/responder/common/responder.h
@@ -176,9 +176,11 @@ responder_get_domain(TALLOC_CTX *sd_mem_ctx, struct resp_ctx *rctx,
 int sss_cmd_empty_packet(struct sss_packet *packet);
 int sss_cmd_send_empty(struct cli_ctx *cctx, TALLOC_CTX *freectx);
 int sss_cmd_send_error(struct cli_ctx *cctx, int err);
-int sss_cmd_execute(struct cli_ctx *cctx, struct sss_cmd_table *sss_cmds);
 void sss_cmd_done(struct cli_ctx *cctx, void *freectx);
 int sss_cmd_get_version(struct cli_ctx *cctx);
+int sss_cmd_execute(struct cli_ctx *cctx,
+                    enum sss_cli_command cmd,
+                    struct sss_cmd_table *sss_cmds);
 struct cli_protocol_version *register_cli_protocol_version(void);
 
 struct setent_req_list;
diff --git a/src/responder/common/responder_cmd.c b/src/responder/common/responder_cmd.c
index cb57cba1a446cdf6ce1782b8d772b70c16ef1e07..3a3fca9b00682c23490509ebdd533bc01e9bb005 100644
--- a/src/responder/common/responder_cmd.c
+++ b/src/responder/common/responder_cmd.c
@@ -141,13 +141,12 @@ int sss_cmd_get_version(struct cli_ctx *cctx)
     return EOK;
 }
 
-int sss_cmd_execute(struct cli_ctx *cctx, struct sss_cmd_table *sss_cmds)
+int sss_cmd_execute(struct cli_ctx *cctx,
+                    enum sss_cli_command cmd,
+                    struct sss_cmd_table *sss_cmds)
 {
-    enum sss_cli_command cmd;
     int i;
 
-    cmd = sss_packet_get_cmd(cctx->creq->in);
-
     for (i = 0; sss_cmds[i].cmd != SSS_CLI_NULL; i++) {
         if (cmd == sss_cmds[i].cmd) {
             return sss_cmds[i].fn(cctx);
@@ -156,7 +155,6 @@ int sss_cmd_execute(struct cli_ctx *cctx, struct sss_cmd_table *sss_cmds)
 
     return EINVAL;
 }
-
 struct setent_req_list {
     struct setent_req_list *prev;
     struct setent_req_list *next;
diff --git a/src/responder/common/responder_common.c b/src/responder/common/responder_common.c
index c7aead3e23e25f25023cbebdcc8f65a8f0275615..d9de920e68a2677924ac16acee96483d549317cc 100644
--- a/src/responder/common/responder_common.c
+++ b/src/responder/common/responder_common.c
@@ -262,6 +262,14 @@ static void client_send(struct cli_ctx *cctx)
     return;
 }
 
+static int client_cmd_execute(struct cli_ctx *cctx, struct sss_cmd_table *sss_cmds)
+{
+    enum sss_cli_command cmd;
+
+    cmd = sss_packet_get_cmd(cctx->creq->in);
+    return sss_cmd_execute(cctx, cmd, sss_cmds);
+}
+
 static void client_recv(struct cli_ctx *cctx)
 {
     int ret;
@@ -291,7 +299,7 @@ static void client_recv(struct cli_ctx *cctx)
         /* do not read anymore */
         TEVENT_FD_NOT_READABLE(cctx->cfde);
         /* execute command */
-        ret = sss_cmd_execute(cctx, cctx->rctx->sss_cmds);
+        ret = client_cmd_execute(cctx, cctx->rctx->sss_cmds);
         if (ret != EOK) {
             DEBUG(0, ("Failed to execute request, aborting client!\n"));
             talloc_free(cctx);
-- 
1.8.1.2

-------------- next part --------------
>From 833e3c7defcdcce32cd0ed6ee19bff1c0c385107 Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhrozek at redhat.com>
Date: Tue, 12 Feb 2013 17:40:01 +0100
Subject: [PATCH 4/4] CMocka based test for the NSS responder

---
 Makefile.am                        |  33 ++
 src/tests/responder/test_nss_srv.c | 635 +++++++++++++++++++++++++++++++++++++
 2 files changed, 668 insertions(+)
 create mode 100644 src/tests/responder/test_nss_srv.c

diff --git a/Makefile.am b/Makefile.am
index 64708216103e48facf780f246de5668c8e0b26a7..ff8994445111e92a3abcab29fbb68aa2ffca9546 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -138,9 +138,15 @@ endif
 
 endif
 
+if HAVE_CMOCKA
+    non_interactive_cmocka_based_tests = \
+	nss-srv-tests
+endif
+
 check_PROGRAMS = \
     stress-tests \
     krb5-child-test \
+    $(non_interactive_cmocka_based_tests) \
     $(non_interactive_check_based_tests)
 
 PYTHON_TESTS =
@@ -153,6 +159,7 @@ endif
 
 TESTS = \
     $(PYTHON_TESTS) \
+    $(non_interactive_cmocka_based_tests) \
     $(non_interactive_check_based_tests)
 
 sssdlib_LTLIBRARIES = \
@@ -1142,6 +1149,32 @@ krb5_child_test_LDADD = \
     libsss_util.la \
     libsss_test_common.la
 
+nss_srv_tests_DEPENDENCIES = \
+     $(ldblib_LTLIBRARIES)
+nss_srv_tests_SOURCES = \
+     src/tests/responder/test_nss_srv.c \
+     src/tests/common.c \
+     src/tests/common_dom.c \
+     src/responder/nss/nsssrv_cmd.c \
+     src/responder/nss/nsssrv_netgroup.c \
+     src/responder/nss/nsssrv_services.c \
+     src/responder/nss/nsssrv_mmap_cache.c \
+     src/responder/common/negcache.c \
+     src/responder/common/responder_packet.c \
+     src/responder/common/responder_cmd.c \
+     src/responder/common/responder_common.c
+nss_srv_tests_CFLAGS = \
+    $(AM_CFLAGS)
+nss_srv_tests_LDFLAGS = \
+    -Wl,-wrap,sss_ncache_check_user \
+    -Wl,-wrap,sss_packet_get_body \
+    -Wl,-wrap,sss_packet_get_cmd \
+    -Wl,-wrap,sss_cmd_send_empty \
+    -Wl,-wrap,sss_cmd_done
+nss_srv_tests_LDADD = \
+    $(CMOCKA_LIBS) \
+    libsss_util.la
+
 noinst_PROGRAMS = pam_test_client
 if BUILD_SUDO
 noinst_PROGRAMS += sss_sudo_cli
diff --git a/src/tests/responder/test_nss_srv.c b/src/tests/responder/test_nss_srv.c
new file mode 100644
index 0000000000000000000000000000000000000000..aecb9ddfa1e13aa31a1cfe54752260d2e88ae4d2
--- /dev/null
+++ b/src/tests/responder/test_nss_srv.c
@@ -0,0 +1,635 @@
+/*
+    Authors:
+        Jakub Hrozek <jhrozek at redhat.com>
+
+    Copyright (C) 2013 Red Hat
+
+    SSSD tests: Common utilities for tests that exercise domains
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <talloc.h>
+#include <tevent.h>
+#include <errno.h>
+#include <popt.h>
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+
+#include <cmocka.h>
+
+#include "confdb/confdb.h"
+#include "db/sysdb.h"
+#include "util/util.h"
+#include "tests/common.h"
+#include "responder/common/responder.h"
+#include "responder/common/negcache.h"
+#include "responder/nss/nsssrv.h"
+#include "responder/nss/nsssrv_private.h"
+
+#define TESTS_PATH "tests_nss"
+#define TEST_CONF_DB "test_nss_conf.ldb"
+#define TEST_SYSDB_FILE "cache_nss_test.ldb"
+#define TEST_DOM_NAME "nss_test"
+#define TEST_ID_PROVIDER "ldap"
+
+struct nss_test_ctx {
+    struct sss_test_ctx *tctx;
+
+    /* The above could be moved to a common struct */
+    struct resp_ctx *rctx;
+    struct cli_ctx *cctx;
+    struct sss_cmd_table *nss_cmds;
+    struct nss_ctx *nctx;
+
+    bool ncache_hit;
+};
+
+struct nss_test_ctx *nss_test_ctx;
+
+/* Mock common structures */
+struct resp_ctx *
+mock_rctx(TALLOC_CTX *mem_ctx,
+          struct tevent_context *ev,
+          struct sss_domain_info *domains,
+          void *pvt_ctx)
+{
+    struct resp_ctx *rctx;
+    errno_t ret;
+
+    rctx = talloc_zero(mem_ctx, struct resp_ctx);
+    if (!rctx) return NULL;
+
+    ret = sss_hash_create(rctx, 30, &rctx->dp_request_table);
+    if (ret != EOK) {
+        talloc_free(rctx);
+        return NULL;
+    }
+
+    rctx->ev = ev;
+    rctx->domains = domains;
+    rctx->pvt_ctx = pvt_ctx;
+    return rctx;
+}
+
+struct cli_ctx *
+mock_cctx(TALLOC_CTX *mem_ctx, struct resp_ctx *rctx)
+{
+    struct cli_ctx *cctx;
+
+    cctx = talloc_zero(mem_ctx, struct cli_ctx);
+    if (!cctx) return NULL;
+
+    cctx->creq = talloc_zero(cctx, struct cli_request);
+    if (cctx->creq == NULL) {
+        talloc_free(cctx);
+        return NULL;
+    }
+
+    cctx->rctx = rctx;
+    return cctx;
+}
+
+/* Mock NSS structure */
+struct nss_ctx *
+mock_nctx(TALLOC_CTX *mem_ctx)
+{
+    struct nss_ctx *nctx;
+    errno_t ret;
+
+    nctx = talloc_zero(mem_ctx, struct nss_ctx);
+    if (!nctx) {
+        return NULL;
+    }
+
+    ret = sss_ncache_init(nctx, &nctx->ncache);
+    if (ret != EOK) {
+        talloc_free(nctx);
+        return NULL;
+    }
+    nctx->neg_timeout = 10;
+
+    return nctx;
+}
+
+/* Mock DP requests */
+struct tevent_req *
+sss_dp_get_account_send(TALLOC_CTX *mem_ctx,
+                        struct resp_ctx *rctx,
+                        struct sss_domain_info *dom,
+                        bool fast_reply,
+                        enum sss_dp_acct_type type,
+                        const char *opt_name,
+                        uint32_t opt_id,
+                        const char *extra)
+{
+    return mock_request_send(mem_ctx, rctx->ev);
+}
+
+typedef int (*acct_cb_t)(void *);
+
+errno_t
+sss_dp_get_account_recv(TALLOC_CTX *mem_ctx,
+                        struct tevent_req *req,
+                        dbus_uint16_t *dp_err,
+                        dbus_uint32_t *dp_ret,
+                        char **err_msg)
+{
+    acct_cb_t cb;
+
+    *dp_err = (dbus_uint16_t) mock();
+    *dp_ret = (dbus_uint32_t) mock();
+    *dp_ret = (dbus_uint32_t) mock();
+
+    cb = (acct_cb_t) mock();
+    if (cb) {
+        (cb)((void *) mock());
+    }
+
+    return mock_request_recv(req);
+}
+
+static void mock_account_recv(uint16_t dp_err, uint32_t dp_ret, char *msg,
+                              acct_cb_t acct_cb, void *pvt)
+{
+    will_return(sss_dp_get_account_recv, dp_err);
+    will_return(sss_dp_get_account_recv, dp_ret);
+    will_return(sss_dp_get_account_recv, msg);
+
+    will_return(sss_dp_get_account_recv, acct_cb);
+    if (acct_cb) {
+        will_return(sss_dp_get_account_recv, pvt);
+    }
+}
+
+static void mock_account_recv_simple(void)
+{
+    return mock_account_recv(0, 0, NULL, NULL, NULL);
+}
+
+/* Mock subdomain requests */
+struct tevent_req *
+sss_dp_get_domains_send(TALLOC_CTX *mem_ctx,
+                        struct resp_ctx *rctx,
+                        bool force,
+                        const char *hint)
+{
+    return mock_request_send(mem_ctx, rctx->ev);
+}
+
+errno_t sss_dp_get_domains_recv(struct tevent_req *req)
+{
+    return mock_request_recv(req);
+}
+
+/* Mock reading requests from a client. Use values passed from mock
+ * instead
+ */
+void __real_sss_packet_get_body(struct sss_packet *packet,
+                                uint8_t **body, size_t *blen);
+
+void __wrap_sss_packet_get_body(struct sss_packet *packet,
+                                uint8_t **body, size_t *blen)
+{
+    bool real = (bool) mock();
+
+    if (real) {
+        return __real_sss_packet_get_body(packet, body, blen);
+    }
+
+    *body = (uint8_t *) mock();
+    *blen = strlen((const char *) *body)+1;
+    return;
+}
+
+/* Mock returning result to client. Terminate the unit test instead. */
+typedef int (*cmd_cb_fn_t)(uint8_t *, size_t );
+
+static void set_cmd_cb(cmd_cb_fn_t fn)
+{
+    will_return(__wrap_sss_cmd_done, fn);
+}
+
+void __wrap_sss_cmd_done(struct cli_ctx *cctx, void *freectx)
+{
+    struct sss_packet *packet = cctx->creq->out;
+    uint8_t *body;
+    size_t blen;
+    cmd_cb_fn_t check_cb;
+
+    check_cb = (cmd_cb_fn_t) mock();
+
+    __real_sss_packet_get_body(packet, &body, &blen);
+
+    nss_test_ctx->tctx->error = check_cb(body, blen);
+    nss_test_ctx->tctx->done = true;
+}
+
+enum sss_cli_command __wrap_sss_packet_get_cmd(struct sss_packet *packet)
+{
+    return (enum sss_cli_command) mock();
+}
+
+int __wrap_sss_cmd_send_empty(struct cli_ctx *cctx, TALLOC_CTX *freectx)
+{
+    nss_test_ctx->tctx->done = true;
+    nss_test_ctx->tctx->error = ENOENT;
+    return EOK;
+}
+
+/* Intercept negative cache lookups */
+int __real_sss_ncache_check_user(struct sss_nc_ctx *ctx, int ttl,
+                                 struct sss_domain_info *dom, const char *name);
+
+int __wrap_sss_ncache_check_user(struct sss_nc_ctx *ctx, int ttl,
+                                 struct sss_domain_info *dom, const char *name)
+{
+    int ret;
+
+    ret = __real_sss_ncache_check_user(ctx, ttl, dom, name);
+    if (ret == EEXIST) {
+        nss_test_ctx->ncache_hit = true;
+    }
+    return ret;
+}
+
+/* Mock input from the client library */
+static void mock_input_user(const char *username)
+{
+    will_return(__wrap_sss_packet_get_body, false);
+    will_return(__wrap_sss_packet_get_body, username);
+}
+
+static void mock_fill_user(void)
+{
+    /* One packet for the entry and one for num entries */
+    will_return(__wrap_sss_packet_get_body, true);
+    will_return(__wrap_sss_packet_get_body, true);
+}
+
+static int parse_user_packet(uint8_t *body, size_t blen, struct passwd *pwd)
+{
+    size_t rp = 2 * sizeof(uint32_t);
+
+    SAFEALIGN_COPY_UINT32(&pwd->pw_uid, body+rp, &rp);
+    SAFEALIGN_COPY_UINT32(&pwd->pw_gid, body+rp, &rp);
+
+    /* Sequence of null terminated strings (name, passwd, gecos, dir, shell) */
+    pwd->pw_name = (char *) body+rp;
+    rp += strlen(pwd->pw_name) + 1;
+    if (rp >= blen) return EINVAL;
+
+    pwd->pw_gecos = (char *) body+rp;
+    rp += strlen(pwd->pw_gecos) + 1;
+    if (rp >= blen) return EINVAL;
+
+    pwd->pw_dir = (char *) body+rp;
+    rp += strlen(pwd->pw_dir) + 1;
+    if (rp >= blen) return EINVAL;
+
+    pwd->pw_shell = (char *) body+rp;
+    return EOK;
+}
+
+/* ====================== The tests =============================== */
+
+/* Check getting cached and valid user from cache. Account callback will
+ * not be called and test_nss_getpwnam_check will make sure the user is
+ * the same as the test entered before starting
+ */
+static int test_nss_getpwnam_check(uint8_t *body, size_t blen)
+{
+    struct passwd pwd;
+    errno_t ret;
+
+    ret = parse_user_packet(body, blen, &pwd);
+    assert_int_equal(ret, EOK);
+
+    assert_int_equal(pwd.pw_uid, 123);
+    assert_int_equal(pwd.pw_gid, 456);
+    assert_string_equal(pwd.pw_name, "testuser");
+    assert_string_equal(pwd.pw_shell, "/bin/sh");
+    return EOK;
+}
+
+void test_nss_getpwnam(void **state)
+{
+    errno_t ret;
+
+    /* Prime the cache with a valid user */
+    ret = sysdb_add_user(nss_test_ctx->tctx->sysdb,
+                         nss_test_ctx->tctx->dom,
+                         "testuser", 123, 456, "test user",
+                         "/home/testuser", "/bin/sh", NULL,
+                         NULL, 300, 0);
+    assert_int_equal(ret, EOK);
+
+    mock_input_user("testuser");
+    will_return(__wrap_sss_packet_get_cmd, SSS_NSS_GETPWNAM);
+    mock_fill_user();
+
+    /* Query for that user, call a callback when command finishes */
+    set_cmd_cb(test_nss_getpwnam_check);
+    ret = sss_cmd_execute(nss_test_ctx->cctx, SSS_NSS_GETPWNAM,
+                          nss_test_ctx->nss_cmds);
+    assert_int_equal(ret, EOK);
+
+    /* Wait until the test finishes with EOK */
+    ret = test_dom_loop(nss_test_ctx->tctx);
+    assert_int_equal(ret, EOK);
+}
+
+/* Test that searching for a nonexistant user yields ENOENT.
+ * Account callback will be called
+ */
+void test_nss_getpwnam_neg(void **state)
+{
+    errno_t ret;
+
+    mock_input_user("testuser_neg");
+    mock_account_recv_simple();
+
+    assert_true(nss_test_ctx->ncache_hit == false);
+
+    ret = sss_cmd_execute(nss_test_ctx->cctx, SSS_NSS_GETPWNAM,
+                          nss_test_ctx->nss_cmds);
+    assert_int_equal(ret, EOK);
+
+    /* Wait until the test finishes with ENOENT */
+    ret = test_dom_loop(nss_test_ctx->tctx);
+    assert_int_equal(ret, ENOENT);
+    assert_true(nss_test_ctx->ncache_hit == false);
+
+    /* Test that subsequent search for a nonexistent user yields
+     * ENOENT and Account callback is not called, on the other hand
+     * the ncache functions will be called
+     */
+    nss_test_ctx->tctx->done = false;
+
+    mock_input_user("testuser_neg");
+    ret = sss_cmd_execute(nss_test_ctx->cctx, SSS_NSS_GETPWNAM,
+                          nss_test_ctx->nss_cmds);
+    assert_int_equal(ret, EOK);
+
+    /* Wait until the test finishes with ENOENT */
+    ret = test_dom_loop(nss_test_ctx->tctx);
+    assert_int_equal(ret, ENOENT);
+    /* Negative cache was hit this time */
+    assert_true(nss_test_ctx->ncache_hit == true);
+}
+
+static int test_nss_getpwnam_search_acct_cb(void *pvt)
+{
+    errno_t ret;
+    struct nss_test_ctx *ctx = talloc_get_type(pvt, struct nss_test_ctx);
+
+    ret = sysdb_add_user(ctx->tctx->sysdb,
+                         ctx->tctx->dom,
+                         "testuser_search", 567, 890, "test search",
+                         "/home/testsearch", "/bin/sh", NULL,
+                         NULL, 300, 0);
+    assert_int_equal(ret, EOK);
+
+    return EOK;
+}
+
+static int test_nss_getpwnam_search_check(uint8_t *body, size_t blen)
+{
+    struct passwd pwd;
+    errno_t ret;
+
+    ret = parse_user_packet(body, blen, &pwd);
+    assert_int_equal(ret, EOK);
+
+    assert_int_equal(pwd.pw_uid, 567);
+    assert_int_equal(pwd.pw_gid, 890);
+    assert_string_equal(pwd.pw_name, "testuser_search");
+    assert_string_equal(pwd.pw_shell, "/bin/sh");
+    return EOK;
+}
+
+void test_nss_getpwnam_search(void **state)
+{
+    errno_t ret;
+    struct ldb_result *res;
+
+    mock_input_user("testuser_search");
+    mock_account_recv(0, 0, NULL, test_nss_getpwnam_search_acct_cb, nss_test_ctx);
+    will_return(__wrap_sss_packet_get_cmd, SSS_NSS_GETPWNAM);
+    mock_fill_user();
+    set_cmd_cb(test_nss_getpwnam_search_check);
+
+    ret = sysdb_getpwnam(nss_test_ctx, nss_test_ctx->tctx->sysdb,
+                         nss_test_ctx->tctx->dom, "testuser_search",
+                         &res);
+    assert_int_equal(ret, EOK);
+    assert_int_equal(res->count, 0);
+
+    ret = sss_cmd_execute(nss_test_ctx->cctx, SSS_NSS_GETPWNAM,
+                          nss_test_ctx->nss_cmds);
+    assert_int_equal(ret, EOK);
+
+    /* Wait until the test finishes with EOK */
+    ret = test_dom_loop(nss_test_ctx->tctx);
+    assert_int_equal(ret, EOK);
+
+    /* test_nss_getpwnam_search_check will check the user attributes */
+    ret = sysdb_getpwnam(nss_test_ctx, nss_test_ctx->tctx->sysdb,
+                         nss_test_ctx->tctx->dom, "testuser_search",
+                         &res);
+    assert_int_equal(ret, EOK);
+    assert_int_equal(res->count, 1);
+}
+
+/* Test that searching for a user that is expired in the cache goes to the DP
+ * which updates the record and the NSS responder returns the updated record
+ *
+ * The user's shell attribute is updated.
+ */
+static int test_nss_getpwnam_update_acct_cb(void *pvt)
+{
+    errno_t ret;
+    struct nss_test_ctx *ctx = talloc_get_type(pvt, struct nss_test_ctx);
+
+    ret = sysdb_store_user(ctx->tctx->sysdb,
+                           ctx->tctx->dom,
+                           "testuser_update", NULL, 10, 11, "test user",
+                           "/home/testuser", "/bin/ksh", NULL,
+                           NULL, NULL, 300, 0);
+    assert_int_equal(ret, EOK);
+
+    return EOK;
+}
+
+static int test_nss_getpwnam_update_check(uint8_t *body, size_t blen)
+{
+    struct passwd pwd;
+    errno_t ret;
+
+    ret = parse_user_packet(body, blen, &pwd);
+    assert_int_equal(ret, EOK);
+
+    assert_int_equal(pwd.pw_uid, 10);
+    assert_int_equal(pwd.pw_gid, 11);
+    assert_string_equal(pwd.pw_name, "testuser_update");
+    assert_string_equal(pwd.pw_shell, "/bin/ksh");
+    return EOK;
+}
+
+void test_nss_getpwnam_update(void **state)
+{
+    errno_t ret;
+    struct ldb_result *res;
+    const char *shell;
+
+    /* Prime the cache with a valid but expired user */
+    ret = sysdb_add_user(nss_test_ctx->tctx->sysdb,
+                         nss_test_ctx->tctx->dom,
+                         "testuser_update", 10, 11, "test user",
+                         "/home/testuser", "/bin/sh", NULL,
+                         NULL, 1, 1);
+    assert_int_equal(ret, EOK);
+
+    /* Mock client input */
+    mock_input_user("testuser_update");
+    /* Mock client command */
+    will_return(__wrap_sss_packet_get_cmd, SSS_NSS_GETPWNAM);
+    /* Call this function when user is updated by the mock DP request */
+    mock_account_recv(0, 0, NULL, test_nss_getpwnam_update_acct_cb, nss_test_ctx);
+    /* Call this function to check what the responder returned to the client */
+    set_cmd_cb(test_nss_getpwnam_update_check);
+    /* Mock output buffer */
+    mock_fill_user();
+
+    /* Fire the command */
+    ret = sss_cmd_execute(nss_test_ctx->cctx, SSS_NSS_GETPWNAM,
+                          nss_test_ctx->nss_cmds);
+    assert_int_equal(ret, EOK);
+
+    /* Wait until the test finishes with EOK */
+    ret = test_dom_loop(nss_test_ctx->tctx);
+    assert_int_equal(ret, EOK);
+
+    /* Check the user was updated in the cache */
+    ret = sysdb_getpwnam(nss_test_ctx, nss_test_ctx->tctx->sysdb,
+                         nss_test_ctx->tctx->dom, "testuser_update",
+                         &res);
+    assert_int_equal(ret, EOK);
+    assert_int_equal(res->count, 1);
+
+    shell = ldb_msg_find_attr_as_string(res->msgs[0], SYSDB_SHELL, NULL);
+    assert_string_equal(shell, "/bin/ksh");
+}
+
+/* Testsuite setup and teardown */
+void nss_test_setup(void **state)
+{
+    errno_t ret;
+    struct sss_test_conf_param params[] = {
+        { "enumerate", "false" },
+        { NULL, NULL },             /* Sentinel */
+    };
+
+    nss_test_ctx = talloc_zero(NULL, struct nss_test_ctx);
+    assert_non_null(nss_test_ctx);
+
+    nss_test_ctx->tctx = create_dom_test_ctx(nss_test_ctx, TESTS_PATH, TEST_CONF_DB,
+                                             TEST_SYSDB_FILE, TEST_DOM_NAME,
+                                             TEST_ID_PROVIDER, params);
+    assert_non_null(nss_test_ctx->tctx);
+
+    nss_test_ctx->nss_cmds = get_nss_cmds();
+    assert_non_null(nss_test_ctx->nss_cmds);
+
+    /* FIXME - perhaps this should be folded into sssd_domain_init or stricty
+     * used together
+     */
+    ret = sss_names_init(nss_test_ctx, nss_test_ctx->tctx->confdb,
+                         TEST_DOM_NAME, &nss_test_ctx->tctx->dom->names);
+    assert_int_equal(ret, EOK);
+
+    /* Initialize the NSS responder */
+    nss_test_ctx->nctx = mock_nctx(nss_test_ctx);
+    assert_non_null(nss_test_ctx->nctx);
+
+    nss_test_ctx->rctx = mock_rctx(nss_test_ctx, nss_test_ctx->tctx->ev,
+                                   nss_test_ctx->tctx->dom, nss_test_ctx->nctx);
+    assert_non_null(nss_test_ctx->rctx);
+
+    /* Create client context */
+    nss_test_ctx->cctx = mock_cctx(nss_test_ctx, nss_test_ctx->rctx);
+    assert_non_null(nss_test_ctx->cctx);
+}
+
+void nss_test_teardown(void **state)
+{
+    talloc_free(nss_test_ctx);
+}
+
+int main(int argc, const char *argv[])
+{
+    int rv;
+    int no_cleanup = 0;
+    poptContext pc;
+    int opt;
+    struct poptOption long_options[] = {
+        POPT_AUTOHELP
+        SSSD_DEBUG_OPTS
+        {"no-cleanup", 'n', POPT_ARG_NONE, &no_cleanup, 0,
+         _("Do not delete the test database after a test run"), NULL },
+        POPT_TABLEEND
+    };
+
+    const UnitTest tests[] = {
+        unit_test_setup_teardown(test_nss_getpwnam,
+                                 nss_test_setup, nss_test_teardown),
+        unit_test_setup_teardown(test_nss_getpwnam_neg,
+                                 nss_test_setup, nss_test_teardown),
+        unit_test_setup_teardown(test_nss_getpwnam_search,
+                                 nss_test_setup, nss_test_teardown),
+        unit_test_setup_teardown(test_nss_getpwnam_update,
+                                 nss_test_setup, nss_test_teardown),
+    };
+
+    /* Set debug level to invalid value so we can deside if -d 0 was used. */
+    debug_level = SSSDBG_INVALID;
+
+    pc = poptGetContext(argv[0], argc, argv, long_options, 0);
+    while((opt = poptGetNextOpt(pc)) != -1) {
+        switch(opt) {
+        default:
+            fprintf(stderr, "\nInvalid option %s: %s\n\n",
+                    poptBadOption(pc, 0), poptStrerror(opt));
+            poptPrintUsage(pc, stderr, 0);
+            return 1;
+        }
+    }
+    poptFreeContext(pc);
+
+    DEBUG_INIT(debug_level);
+
+    /* Even though normally the tests should clean up after themselves
+     * they might not after a failed run. Remove the old db to be sure */
+    tests_set_cwd();
+    test_dom_suite_cleanup(TESTS_PATH, TEST_CONF_DB, TEST_SYSDB_FILE);
+    test_dom_suite_setup(TESTS_PATH);
+
+    rv = run_tests(tests);
+    if (rv == 0 && !no_cleanup) {
+        test_dom_suite_cleanup(TESTS_PATH, TEST_CONF_DB, TEST_SYSDB_FILE);
+    }
+    return rv;
+}
-- 
1.8.1.2



More information about the sssd-devel mailing list