]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-imap-urlauth: Add test-imap-urlauth
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Thu, 5 Feb 2026 09:52:34 +0000 (11:52 +0200)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Thu, 5 Feb 2026 11:52:49 +0000 (11:52 +0000)
src/Makefile.am
src/lib-imap-urlauth/Makefile.am
src/lib-imap-urlauth/test-imap-urlauth.c [new file with mode: 0644]

index 45a17a19458d5cd0247977e825d8ed36136621f5..7b9ab145ca1edcbc16188fd4f3cca79698e9a3e3 100644 (file)
@@ -47,10 +47,10 @@ SUBDIRS = \
        $(LIB_LUA) \
        lib-language \
        lib-imap-client \
-       lib-imap-urlauth \
        lib-compression \
        lib-index \
        lib-storage \
+       lib-imap-urlauth \
        lib-sql \
        lib-lda \
        lib-dict-backend \
index 5b77658691aa29892fd01159f0fa8b3215923387..32cd4f19e98a5cab4d9c8468e47e1e44043aa5de 100644 (file)
@@ -1,3 +1,5 @@
+include $(top_srcdir)/Makefile.test.include
+
 noinst_LTLIBRARIES = libimap-urlauth.la
 
 AM_CPPFLAGS = \
@@ -26,6 +28,15 @@ headers = \
        imap-urlauth-backend.h \
        imap-urlauth-connection.h
 
+test_programs = test-imap-urlauth
+
+test_imap_urlauth_SOURCES = test-imap-urlauth.c
+test_imap_urlauth_LDADD = \
+       imap-urlauth.lo \
+       ../lib-dovecot/libdovecot.la \
+       ../lib-test/libtest.la \
+       $(LIBDOVECOT_TEST_LIBS)
+
 pkginc_libdir=$(pkgincludedir)
 pkginc_lib_HEADERS = $(headers)
 
diff --git a/src/lib-imap-urlauth/test-imap-urlauth.c b/src/lib-imap-urlauth/test-imap-urlauth.c
new file mode 100644 (file)
index 0000000..603be7f
--- /dev/null
@@ -0,0 +1,430 @@
+/* Copyright (c) 2025 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "auth-master.h"
+#include "mail-storage.h"
+#include "imap-url.h"
+#include "imap-urlauth-private.h"
+#include "imap-urlauth-fetch.h"
+#include "imap-urlauth-backend.h"
+#include "imap-urlauth-connection.h"
+#include "imap-msgpart-url.h"
+
+#include <time.h>
+
+/* Mocks */
+int imap_msgpart_url_create(struct mail_user *user ATTR_UNUSED,
+                           const struct imap_url *url ATTR_UNUSED,
+                           struct imap_msgpart_url **mpurl_r,
+                           const char **error_r ATTR_UNUSED)
+{
+       *mpurl_r = (struct imap_msgpart_url *)"mock-mpurl";
+       return 0;
+}
+
+void imap_msgpart_url_free(struct imap_msgpart_url **mpurl ATTR_UNUSED)
+{
+}
+
+int imap_msgpart_url_verify(struct imap_msgpart_url *mpurl ATTR_UNUSED,
+                           const char **error_r ATTR_UNUSED)
+{
+       return 1;
+}
+
+struct mailbox *imap_msgpart_url_get_mailbox(struct imap_msgpart_url *mpurl ATTR_UNUSED)
+{
+       return (struct mailbox *)"mock-mailbox";
+}
+
+int imap_msgpart_url_open_mailbox(struct imap_msgpart_url *mpurl ATTR_UNUSED,
+                                 struct mailbox **box_r,
+                                 enum mail_error *error_code_r ATTR_UNUSED,
+                                 const char **error_r ATTR_UNUSED)
+{
+       *box_r = (struct mailbox *)"mock-mailbox";
+       return 1;
+}
+
+int imap_urlauth_backend_get_mailbox_key(struct mailbox *box ATTR_UNUSED,
+                                        bool create ATTR_UNUSED,
+                                        unsigned char key_r[IMAP_URLAUTH_KEY_LEN],
+                                        const char **client_error_r ATTR_UNUSED,
+                                        enum mail_error *error_code_r ATTR_UNUSED)
+{
+       memset(key_r, 0, IMAP_URLAUTH_KEY_LEN);
+       return 1;
+}
+
+int imap_urlauth_backend_reset_mailbox_key(struct mailbox *box ATTR_UNUSED)
+{
+       return 0;
+}
+
+int imap_urlauth_backend_reset_all_keys(struct mail_user *user ATTR_UNUSED)
+{
+       return 0;
+}
+
+int auth_master_user_lookup(struct auth_master_connection *conn ATTR_UNUSED,
+                           const char *user ATTR_UNUSED,
+                           const struct auth_user_info *info ATTR_UNUSED,
+                           pool_t pool ATTR_UNUSED,
+                           const char **username_r ATTR_UNUSED,
+                           const char *const **fields_r ATTR_UNUSED)
+{
+       return 0;
+}
+struct auth_master_connection *mail_user_auth_master_conn = NULL;
+
+struct imap_urlauth_connection *
+imap_urlauth_connection_init(const char *path ATTR_UNUSED,
+                            const char *service ATTR_UNUSED,
+                            struct mail_user *user ATTR_UNUSED,
+                            const char *session_id ATTR_UNUSED,
+                            unsigned int idle_timeout_msecs ATTR_UNUSED)
+{
+       return (struct imap_urlauth_connection *)"mock-conn";
+}
+
+void imap_urlauth_connection_deinit(struct imap_urlauth_connection **conn)
+{
+       *conn = NULL;
+}
+
+static void test_imap_urlauth_check(void)
+{
+       struct imap_urlauth_context uctx = {
+               .url_host = "localhost",
+               .url_port = 143,
+               .access_service = "imap",
+               .access_user = "user",
+       };
+       struct imap_url url = {
+               .mailbox = "INBOX",
+               .uid = 1,
+               .uauth_rumpurl = "imap://user@localhost/INBOX/;UID=1;URLAUTH=anonymous",
+               .uauth_mechanism = "INTERNAL",
+               .userid = "user",
+               .uauth_access_application = "anonymous",
+               .host = { .name = "localhost" },
+               .uauth_expire = (time_t)-1,
+       };
+       const char *error;
+       bool result;
+       time_t now = time(NULL);
+
+       test_begin("imap_urlauth_check()");
+
+       /* 1. Success case */
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(valid)", result);
+
+       /* 2. Invalid URL fields */
+       url.mailbox = NULL;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(missing mailbox)", !result);
+       test_assert_strcmp(error, "Invalid URL: Must be a full URLAUTH URL");
+       url.mailbox = "INBOX";
+
+       url.uid = 0;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(missing uid)", !result);
+       test_assert_strcmp(error, "Invalid URL: Must be a full URLAUTH URL");
+       url.uid = 1;
+
+       url.search_program = "ALL";
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(search program not allowed)", !result);
+       test_assert_strcmp(error, "Invalid URL: Must be a full URLAUTH URL");
+       url.search_program = NULL;
+
+       url.uauth_rumpurl = NULL;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(missing rumpurl)", !result);
+       test_assert_strcmp(error, "Invalid URL: Must be a full URLAUTH URL");
+       url.uauth_rumpurl = "imap://user@localhost/INBOX/;UID=1;URLAUTH=anonymous";
+
+       url.uauth_mechanism = NULL;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(missing mechanism)", !result);
+       test_assert_strcmp(error, "Invalid URL: Must be a full URLAUTH URL");
+       url.uauth_mechanism = "INTERNAL";
+
+       /* 3. Missing userid */
+       url.userid = NULL;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(missing userid)", !result);
+       test_assert_strcmp(error, "Invalid URLAUTH: Missing user name");
+       url.userid = "user";
+
+       /* 4. Unsupported mechanism */
+       url.uauth_mechanism = "UNKNOWN";
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(unsupported mechanism)", !result);
+       test_assert_strcmp(error, "Unsupported URLAUTH mechanism: UNKNOWN");
+       url.uauth_mechanism = "INTERNAL";
+
+       /* 5. Expired */
+       url.uauth_expire = now - 1;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(expired)", !result);
+       test_assert_strcmp(error, "URLAUTH has expired");
+       url.uauth_expire = now + 3600;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(not expired)", result);
+       url.uauth_expire = (time_t)-1;
+
+       /* 6. Host/port check */
+       url.host.name = "otherhost";
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(invalid host)", !result);
+       test_assert_strcmp(error, "Invalid URL: Inappropriate host name");
+       url.host.name = "localhost";
+
+       uctx.url_host = "*";
+       url.host.name = "anyhost";
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(any host allowed)", result);
+       uctx.url_host = "localhost";
+       url.host.name = "localhost";
+
+       url.port = 993;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(invalid port)", !result);
+       test_assert_strcmp(error, "Invalid URL: Inappropriate server port");
+       url.port = 0; /* Default 143 */
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(default port 143)", result);
+
+       /* 7. Access checks - IMAP */
+       uctx.access_service = "imap";
+
+       /* application=user */
+       url.uauth_access_application = "user";
+       url.uauth_access_user = "testuser";
+       uctx.access_user = "testuser";
+       uctx.access_anonymous = FALSE;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap user access - match)", result);
+
+       uctx.access_user = "other";
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap user access - mismatch)", !result);
+       test_assert_strcmp(error, "No 'user+testuser' access allowed for user other");
+
+       uctx.access_anonymous = TRUE;
+       uctx.access_user = "anonymous";
+       url.uauth_access_user = "anonymous";
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap user access - anonymous not allowed)", !result);
+       test_assert_strcmp(error, "No 'user+anonymous' access allowed for anonymous user");
+
+       /* application=authuser */
+       url.uauth_access_application = "authuser";
+       url.uauth_access_user = NULL;
+       uctx.access_anonymous = FALSE;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap authuser access)", result);
+       uctx.access_anonymous = TRUE;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap authuser access - anonymous not allowed)", !result);
+       test_assert_strcmp(error, "No 'authuser' access allowed for anonymous user");
+
+       /* application=anonymous */
+       url.uauth_access_application = "anonymous";
+       uctx.access_anonymous = TRUE;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap anonymous access)", result);
+
+       /* custom applications */
+       const char *apps[] = { "app1", "app2+", NULL };
+       uctx.access_applications = apps;
+       uctx.access_anonymous = FALSE;
+       uctx.access_user = "testuser";
+
+       url.uauth_access_application = "app1";
+       url.uauth_access_user = NULL;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap custom app1 access)", result);
+
+       url.uauth_access_application = "app2";
+       url.uauth_access_user = "someuser";
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap custom app2+ access)", result);
+
+       url.uauth_access_application = "app2";
+       url.uauth_access_user = NULL;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap custom app2+ access - no user)", !result);
+       test_assert_strcmp(error, "No 'app2' access allowed for user testuser");
+
+       url.uauth_access_application = "app2prefix";
+       url.uauth_access_user = "someuser";
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap custom app2prefix access)", !result);
+       test_assert_strcmp(error, "No 'app2prefix+someuser' access allowed for user testuser");
+
+       url.uauth_access_application = "app3";
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(imap unknown app access)", !result);
+       test_assert_strcmp(error, "No 'app3+someuser' access allowed for user testuser");
+
+       result = imap_urlauth_check(&uctx, &url, TRUE, &error);
+       test_out("imap_urlauth_check(imap unknown app access - ignore_unknown)", result);
+
+       /* 8. Access checks - Submission */
+       uctx.access_service = "submission";
+       url.uauth_access_application = "submit";
+       url.uauth_access_user = "testuser";
+       uctx.access_user = "testuser";
+       uctx.access_anonymous = FALSE;
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(submission submit access)", result);
+
+       url.uauth_access_application = "user";
+       result = imap_urlauth_check(&uctx, &url, FALSE, &error);
+       test_out("imap_urlauth_check(submission user access - not allowed)", !result);
+       test_assert_strcmp(error, "No 'user+testuser' access allowed for submission service");
+
+       test_end();
+}
+
+static void test_imap_urlauth_generate(void)
+{
+       struct mail_user user = {
+               .username = "user",
+       };
+       struct imap_urlauth_context uctx = {
+               .user = &user,
+               .url_host = "localhost",
+               .url_port = 143,
+               .access_service = "imap",
+               .access_user = "user",
+       };
+       const char *urlauth, *error;
+       int ret;
+
+       test_begin("imap_urlauth_generate()");
+
+       /* 1. Unsupported mechanism */
+       ret = imap_urlauth_generate(&uctx, "UNKNOWN", "imap://user@localhost/INBOX/;UID=1;URLAUTH=user+user", &urlauth, &error);
+       test_out("imap_urlauth_generate(unsupported mechanism)", ret == 0 && error != NULL);
+       test_assert_strcmp(error, "Unsupported URLAUTH mechanism: UNKNOWN");
+
+       /* 2. Invalid URL */
+       ret = imap_urlauth_generate(&uctx, "INTERNAL", "invalid-url", &urlauth, &error);
+       test_out("imap_urlauth_generate(invalid url)", ret == 0 && error != NULL);
+       test_assert_strcmp(error, "Invalid URL: Relative URL not allowed");
+
+       /* 3. Not a rump URL (has search program) */
+       ret = imap_urlauth_generate(&uctx, "INTERNAL", "imap://user@localhost/INBOX?ALL", &urlauth, &error);
+       test_out("imap_urlauth_generate(not a rump url - search)", ret == 0 && error != NULL);
+       test_assert_strcmp(error, "Invalid URL: Must be an URLAUTH rump URL");
+
+       /* 4. Missing userid */
+       ret = imap_urlauth_generate(&uctx, "INTERNAL", "imap://localhost/INBOX/;UID=1;URLAUTH=user+user", &urlauth, &error);
+       test_out("imap_urlauth_generate(missing userid)", ret == 0 && error != NULL);
+       test_assert_strcmp(error, "Invalid URL: Missing user name");
+
+       /* 5. Anonymous user not permitted */
+       user.anonymous = TRUE;
+       ret = imap_urlauth_generate(&uctx, "INTERNAL", "imap://user@localhost/INBOX/;UID=1;URLAUTH=user+user", &urlauth, &error);
+       test_out("imap_urlauth_generate(anonymous not permitted)", ret == 0 && error != NULL);
+       test_assert_strcmp(error, "Anonymous logins not permitted to generate URLAUTH");
+       user.anonymous = FALSE;
+
+       /* 6. User mismatch */
+       user.username = "other";
+       ret = imap_urlauth_generate(&uctx, "INTERNAL", "imap://wronguser@localhost/INBOX/;UID=1;URLAUTH=user+user", &urlauth, &error);
+       test_out("imap_urlauth_generate(user mismatch)", ret == 0 && error != NULL);
+       test_assert_strcmp(error, "Not permitted to generate URLAUTH for other user wronguser");
+       user.username = "user";
+
+       /* 7. Host mismatch */
+       ret = imap_urlauth_generate(&uctx, "INTERNAL", "imap://user@otherhost/INBOX/;UID=1;URLAUTH=user+user", &urlauth, &error);
+       test_out("imap_urlauth_generate(host mismatch)", ret == 0 && error != NULL);
+       test_assert_strcmp(error, "Invalid URL: Inappropriate host name");
+
+       /* 8. Success path (with mocks) */
+       ret = imap_urlauth_generate(&uctx, "INTERNAL", "imap://user@localhost/INBOX/;UID=1;URLAUTH=user+user", &urlauth, &error);
+       test_out("imap_urlauth_generate(success)", ret == 1 && urlauth != NULL);
+
+       test_end();
+}
+
+static void test_imap_urlauth_fetch(void)
+{
+       struct mail_user user = {
+               .username = "user",
+       };
+       struct imap_urlauth_context uctx = {
+               .user = &user,
+               .url_host = "localhost",
+               .url_port = 143,
+               .access_service = "imap",
+               .access_user = "user",
+       };
+       struct imap_url url = {
+               .mailbox = "INBOX",
+               .uid = 1,
+               .uauth_rumpurl = "imap://user@localhost/INBOX/;UID=1;URLAUTH=anonymous",
+               .uauth_mechanism = "INTERNAL",
+               .userid = "user",
+               .uauth_access_application = "anonymous",
+               .host = {
+                       .name = "localhost",
+               },
+               .uauth_expire = (time_t)-1,
+               .uauth_token = (const unsigned char *)"12345678901234567890",
+               .uauth_token_size = 20,
+       };
+       struct imap_msgpart_url *mpurl;
+       enum mail_error error_code;
+       const char *error;
+       int ret;
+
+       test_begin("imap_urlauth_fetch()");
+
+       /* 1. imap_urlauth_fetch_parsed failure */
+       url.host.name = "otherhost";
+       ret = imap_urlauth_fetch_parsed(&uctx, &url, &mpurl, &error_code, &error);
+       test_out("imap_urlauth_fetch_parsed(check failure)", ret == 0 && error_code == MAIL_ERROR_PARAMS);
+       url.host.name = "localhost";
+
+       /* 2. imap_urlauth_fetch failure (invalid string) */
+       ret = imap_urlauth_fetch(&uctx, "invalid-url", &mpurl, &error_code, &error);
+       test_out("imap_urlauth_fetch(invalid url)", ret == 0 && error_code == MAIL_ERROR_PARAMS);
+
+       /* 3. imap_urlauth_fetch success path (calls parsed) */
+       ret = imap_urlauth_fetch(&uctx, "imap://user@localhost/INBOX/;UID=1;URLAUTH=anonymous:INTERNAL:4142434445464748494A4B4C4D4E4F5051525354", &mpurl, &error_code, &error);
+       /* Verification failure is expected because our mock key is all zeros but the token is not a SHA1-HMAC of rumpurl with zero key */
+       test_out("imap_urlauth_fetch(verification failure)", ret == 0 && error_code == MAIL_ERROR_PERM);
+
+       test_end();
+}
+
+static void test_imap_urlauth_reset_keys(void)
+{
+       struct imap_urlauth_context uctx = {
+               .user = NULL
+       };
+
+       test_begin("imap_urlauth_reset_keys()");
+       test_out("imap_urlauth_reset_mailbox_key", imap_urlauth_reset_mailbox_key(&uctx, NULL) == 0);
+       test_out("imap_urlauth_reset_all_keys", imap_urlauth_reset_all_keys(&uctx) == 0);
+       test_end();
+}
+
+int main(void)
+{
+       static void (*const test_functions[])(void) = {
+               test_imap_urlauth_check,
+               test_imap_urlauth_generate,
+               test_imap_urlauth_fetch,
+               test_imap_urlauth_reset_keys,
+               NULL
+       };
+       return test_run(test_functions);
+}