diff --git a/lib/pacemaker/tests/pcmk_ticket/Makefile.am b/lib/pacemaker/tests/pcmk_ticket/Makefile.am
index bf512480b8..adb6195bb4 100644
--- a/lib/pacemaker/tests/pcmk_ticket/Makefile.am
+++ b/lib/pacemaker/tests/pcmk_ticket/Makefile.am
@@ -1,21 +1,22 @@
 #
 # Copyright 2024 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
 # This source code is licensed under the GNU General Public License version 2
 # or later (GPLv2+) WITHOUT ANY WARRANTY.
 #
 
 include $(top_srcdir)/mk/tap.mk
 include $(top_srcdir)/mk/unittest.mk
 
 LDADD += $(top_builddir)/lib/pacemaker/libpacemaker.la
 LDADD += $(top_builddir)/lib/cib/libcib.la
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 
 check_PROGRAMS = pcmk_ticket_constraints_test \
-		 pcmk_ticket_get_attr_test
+		 pcmk_ticket_get_attr_test \
+		 pcmk_ticket_info_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_info_test.c b/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_info_test.c
new file mode 100644
index 0000000000..67430eeb51
--- /dev/null
+++ b/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_info_test.c
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/cib/internal.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml.h>
+#include <pacemaker.h>
+
+static char *cib_path = NULL;
+
+static int
+setup_test(void **state)
+{
+    cib_path = pcmk__cib_test_copy_cib("tickets.xml");
+
+    if (cib_path == NULL) {
+        return -1;
+    }
+
+    return 0;
+}
+
+static int
+teardown_test(void **state)
+{
+    pcmk__cib_test_cleanup(cib_path);
+    cib_path = NULL;
+    return 0;
+}
+
+static void
+bad_arguments(void **state)
+{
+    assert_int_equal(pcmk_ticket_info(NULL, "ticketA"), EINVAL);
+}
+
+static void
+unknown_ticket(void **state)
+{
+    xmlNode *xml = NULL;
+
+    assert_int_equal(pcmk_ticket_info(&xml, "XYZ"), ENXIO);
+    pcmk__assert_validates(xml);
+    free_xml(xml);
+}
+
+static void
+all_tickets(void **state)
+{
+    xmlNode *node = NULL;
+    xmlXPathObject *xpath_obj = NULL;
+    xmlNode *xml = NULL;
+
+    assert_int_equal(pcmk_ticket_info(&xml, NULL), pcmk_rc_ok);
+    pcmk__assert_validates(xml);
+
+    /* Verify that the XML result has three <ticket> elements, with the attributes
+     * we expect.  The input has four tickets, but when they are loaded into the
+     * scheduler's hash table, the duplicate IDs will collide leaving us with
+     * three.
+     */
+    xpath_obj = xpath_search(xml, "//" PCMK_XE_PACEMAKER_RESULT "/" PCMK_XE_TICKETS "/" PCMK_XE_TICKET);
+    assert_int_equal(numXpathResults(xpath_obj), 3);
+    freeXpathObject(xpath_obj);
+
+    xpath_obj = xpath_search(xml, "//" PCMK_XE_PACEMAKER_RESULT "/" PCMK_XE_TICKETS
+                                  "/" PCMK_XE_TICKET "[@" PCMK_XA_ID "=\"ticketA\"]");
+
+    node = getXpathResult(xpath_obj, 0);
+    assert_string_equal(crm_element_value(node, PCMK_XA_STATUS), PCMK_VALUE_REVOKED);
+    assert_string_equal(crm_element_value(node, PCMK__XA_GRANTED), "false");
+    assert_string_equal(crm_element_value(node, PCMK_XA_STANDBY), PCMK_VALUE_FALSE);
+    assert_string_equal(crm_element_value(node, "owner"), "1");
+    freeXpathObject(xpath_obj);
+
+    xpath_obj = xpath_search(xml, "//" PCMK_XE_PACEMAKER_RESULT "/" PCMK_XE_TICKETS
+                                  "/" PCMK_XE_TICKET "[@" PCMK_XA_ID "=\"ticketB\"]");
+
+    node = getXpathResult(xpath_obj, 0);
+    assert_string_equal(crm_element_value(node, PCMK_XA_STATUS), PCMK_VALUE_GRANTED);
+    assert_string_equal(crm_element_value(node, PCMK__XA_GRANTED), "true");
+    assert_string_equal(crm_element_value(node, PCMK_XA_STANDBY), PCMK_VALUE_FALSE);
+    assert_null(crm_element_value(node, "owner"));
+    freeXpathObject(xpath_obj);
+
+    xpath_obj = xpath_search(xml, "//" PCMK_XE_PACEMAKER_RESULT "/" PCMK_XE_TICKETS
+                                  "/" PCMK_XE_TICKET "[@" PCMK_XA_ID "=\"ticketC\"]");
+
+    node = getXpathResult(xpath_obj, 0);
+    assert_string_equal(crm_element_value(node, PCMK_XA_STATUS), PCMK_VALUE_GRANTED);
+    assert_string_equal(crm_element_value(node, PCMK__XA_GRANTED), "true");
+    assert_string_equal(crm_element_value(node, PCMK_XA_STANDBY), PCMK_VALUE_FALSE);
+    assert_null(crm_element_value(node, "owner"));
+
+    freeXpathObject(xpath_obj);
+    free_xml(xml);
+}
+
+static void
+single_ticket(void **state)
+{
+    xmlNode *node = NULL;
+    xmlXPathObject *xpath_obj = NULL;
+    xmlNode *xml = NULL;
+
+    assert_int_equal(pcmk_ticket_info(&xml, "ticketA"), pcmk_rc_ok);
+    pcmk__assert_validates(xml);
+
+    /* Verify that the XML result has only one <ticket>, with the attributes
+     * we expect.
+     */
+    xpath_obj = xpath_search(xml, "//" PCMK_XE_PACEMAKER_RESULT "/" PCMK_XE_TICKETS
+                                  "/" PCMK_XE_TICKET "[@" PCMK_XA_ID "=\"ticketA\"]");
+    assert_int_equal(numXpathResults(xpath_obj), 1);
+
+    node = getXpathResult(xpath_obj, 0);
+    assert_string_equal(crm_element_value(node, PCMK_XA_STATUS), PCMK_VALUE_REVOKED);
+    assert_string_equal(crm_element_value(node, PCMK__XA_GRANTED), "false");
+    assert_string_equal(crm_element_value(node, PCMK_XA_STANDBY), PCMK_VALUE_FALSE);
+    assert_string_equal(crm_element_value(node, "owner"), "1");
+
+    freeXpathObject(xpath_obj);
+    free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__cib_test_setup_group, NULL,
+                cmocka_unit_test_setup_teardown(bad_arguments, setup_test, teardown_test),
+                cmocka_unit_test_setup_teardown(unknown_ticket, setup_test, teardown_test),
+                cmocka_unit_test_setup_teardown(all_tickets, setup_test, teardown_test),
+                cmocka_unit_test_setup_teardown(single_ticket, setup_test, teardown_test))