* Copyright (C) 2014 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "config.h"
#include "cockpittransport.h"
#include "cockpitpipe.h"
#include "cockpitpipetransport.h"
#include "testlib/cockpittest.h"
#include "testlib/mock-transport.h"
#include "websocket.h"
#include <glib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#define WAIT_UNTIL(cond) \
G_STMT_START \
while (!(cond)) g_main_context_iteration (NULL, TRUE); \
G_STMT_END
typedef struct {
CockpitTransport *transport;
CockpitPipe *pipe;
} TestCase;
static void
setup_with_child (TestCase *tc,
gconstpointer data)
{
gchar *argv[] = { (gchar *)data, NULL };
GError *error = NULL;
GPid pid;
int in;
int out;
g_spawn_async_with_pipes (NULL, argv, NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
NULL, NULL, &pid, &in, &out, NULL, &error);
g_assert_no_error (error);
tc->pipe = g_object_new (COCKPIT_TYPE_PIPE,
"name", argv[0],
"in-fd", out,
"out-fd", in,
"pid", pid,
NULL);
tc->transport = cockpit_pipe_transport_new (tc->pipe);
}
static void
setup_no_child (TestCase *tc,
gconstpointer data)
{
int sv[2];
if (socketpair (PF_LOCAL, SOCK_STREAM, 0, sv) < 0)
g_assert_not_reached ();
tc->pipe = cockpit_pipe_new ("mock", sv[0], sv[1]);
tc->transport = cockpit_pipe_transport_new (tc->pipe);
}
static void
teardown_transport (TestCase *tc,
gconstpointer data)
{
cockpit_assert_expected ();
g_object_add_weak_pointer (G_OBJECT (tc->transport),
(gpointer *)&tc->transport);
g_object_unref (tc->transport);
g_assert (tc->transport == NULL);
g_object_add_weak_pointer (G_OBJECT (tc->pipe),
(gpointer *)&tc->pipe);
g_object_unref (tc->pipe);
g_assert (tc->pipe == NULL);
}
static gboolean
on_recv_get_payload (CockpitTransport *transport,
const gchar *channel,
GBytes *message,
gpointer user_data)
{
GBytes **received = user_data;
if (channel == NULL)
return FALSE;
g_assert_cmpstr (channel, ==, "546");
g_assert (*received == NULL);
*received = g_bytes_ref (message);
return TRUE;
}
static gboolean
on_recv_multiple (CockpitTransport *transport,
const gchar *channel,
GBytes *message,
gpointer user_data)
{
gint *state = user_data;
GBytes *check = NULL;
if (channel == NULL)
return FALSE;
g_assert_cmpstr (channel, ==, "9");
if (*state == 0)
check = g_bytes_new_static ("one", 3);
else if (*state == 1)
check = g_bytes_new_static ("two", 3);
else
g_assert_not_reached ();
(*state)++;
g_assert (g_bytes_equal (message, check));
g_bytes_unref (check);
return TRUE;
}
static void
on_closed_set_flag (CockpitTransport *transport,
const gchar *problem,
gpointer user_data)
{
gboolean *flag = user_data;
g_assert (problem == NULL);
g_assert (*flag == FALSE);
*flag = TRUE;
}
static void
test_properties (TestCase *tc,
gconstpointer data)
{
CockpitPipe *pipe;
g_assert (cockpit_pipe_transport_get_pipe (COCKPIT_PIPE_TRANSPORT (tc->transport)) == tc->pipe);
g_object_get (tc->transport, "pipe", &pipe, NULL);
g_assert (pipe == tc->pipe);
g_object_unref (pipe);
}
static void
test_echo_and_close (TestCase *tc,
gconstpointer data)
{
GBytes *received = NULL;
GBytes *sent;
gboolean closed = FALSE;
sent = g_bytes_new_static ("the message", 11);
g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_get_payload), &received);
cockpit_transport_send (tc->transport, "546", sent);
WAIT_UNTIL (received != NULL);
g_assert (g_bytes_equal (received, sent));
g_bytes_unref (sent);
g_bytes_unref (received);
received = NULL;
g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
cockpit_transport_close (tc->transport, NULL);
WAIT_UNTIL (closed == TRUE);
}
static void
test_ping_pong (void)
{
JsonObject *reply = NULL;
MockTransport *mock;
CockpitTransport *transport;
GBytes *sent;
mock = mock_transport_new ();
transport = COCKPIT_TRANSPORT (mock);
* A "ping" without a channel is not forwarded, but responded to directly
* by the peer transport. Here we test that CockpitTransport does
* that response properly.
*/
sent = cockpit_transport_build_control ("command", "ping", "value", "blah", "other", "marmalade", NULL);
cockpit_transport_emit_recv (transport, NULL, sent);
g_bytes_unref (sent);
reply = mock_transport_pop_control (mock);
cockpit_assert_json_eq (reply, "{ \"command\": \"pong\", \"value\": \"blah\", \"other\": \"marmalade\" }");
g_object_unref (mock);
}
static void
test_ping_channel (void)
{
JsonObject *reply = NULL;
MockTransport *mock;
CockpitTransport *transport;
GBytes *sent;
mock = mock_transport_new ();
transport = COCKPIT_TRANSPORT (mock);
sent = cockpit_transport_build_control ("command", "ping", "channel", "444", NULL);
cockpit_transport_emit_recv (transport, NULL, sent);
g_bytes_unref (sent);
sent = cockpit_transport_build_control ("command", "ping", "value", "blah", "other", "marmalade", NULL);
cockpit_transport_emit_recv (transport, NULL, sent);
g_bytes_unref (sent);
reply = mock_transport_pop_control (mock);
cockpit_assert_json_eq (reply, "{ \"command\": \"pong\", \"value\": \"blah\", \"other\": \"marmalade\" }");
g_object_unref (mock);
}
static void
test_echo_queue (TestCase *tc,
gconstpointer data)
{
GBytes *sent;
gint state = 0;
gboolean closed = FALSE;
g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_multiple), &state);
g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
sent = g_bytes_new_static ("one", 3);
cockpit_transport_send (tc->transport, "9", sent);
g_bytes_unref (sent);
sent = g_bytes_new_static ("two", 3);
cockpit_transport_send (tc->transport, "9", sent);
g_bytes_unref (sent);
cockpit_transport_close (tc->transport, NULL);
WAIT_UNTIL (state == 2 && closed == TRUE);
}
static void
test_echo_large (TestCase *tc,
gconstpointer data)
{
GBytes *received = NULL;
GBytes *sent;
g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_get_payload), &received);
sent = g_bytes_new_take (g_strnfill (1020, '!'), 1020);
cockpit_transport_send (tc->transport, "546", sent);
WAIT_UNTIL (received != NULL);
g_assert (g_bytes_equal (received, sent));
g_bytes_unref (sent);
g_bytes_unref (received);
received = NULL;
sent = g_bytes_new_take (g_strnfill (10 * 1000 * 1000, '?'), 10 * 1000 * 1000);
cockpit_transport_send (tc->transport, "546", sent);
WAIT_UNTIL (received != NULL);
g_assert (g_bytes_equal (received, sent));
g_bytes_unref (sent);
g_bytes_unref (received);
received = NULL;
sent = g_bytes_new_static ("yello", 5);
cockpit_transport_send (tc->transport, "546", sent);
WAIT_UNTIL (received != NULL);
g_assert (g_bytes_equal (received, sent));
g_bytes_unref (sent);
g_bytes_unref (received);
received = NULL;
}
static void
on_closed_get_problem (CockpitTransport *transport,
const gchar *problem,
gpointer user_data)
{
const gchar **ret = user_data;
g_assert (problem != NULL);
g_assert (*ret == NULL);
*ret = g_strdup (problem);
}
static void
test_close_problem (TestCase *tc,
gconstpointer data)
{
gchar *problem = NULL;
g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
cockpit_transport_close (tc->transport, "right now");
WAIT_UNTIL (problem != NULL);
g_assert_cmpstr (problem, ==, "right now");
g_free (problem);
}
static void
test_terminate_problem (TestCase *tc,
gconstpointer data)
{
gchar *problem = NULL;
GPid pid;
g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
g_assert (cockpit_pipe_get_pid (tc->pipe, &pid));
g_assert (pid != 0);
kill (pid, SIGTERM);
WAIT_UNTIL (problem != NULL);
g_assert_cmpstr (problem, ==, "terminated");
g_free (problem);
}
static void
test_exception_problem (TestCase *tc,
gconstpointer data)
{
gchar *problem = NULL;
g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
WAIT_UNTIL (problem != NULL);
g_assert_cmpstr (problem, ==, "internal-error");
g_free (problem);
}
static void
test_nocockpit_problem (TestCase *tc,
gconstpointer data)
{
gchar *problem = NULL;
g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
WAIT_UNTIL (problem != NULL);
g_assert_cmpstr (problem, ==, "no-cockpit");
g_free (problem);
}
static void
test_read_error (void)
{
CockpitTransport *transport;
gchar *problem = NULL;
gint fds[2];
g_assert (write (1000, "1", 1) < 0);
g_assert_cmpint (pipe (fds), ==, 0);
g_test_expect_message ("cockpit-protocol", G_LOG_LEVEL_WARNING, "*Bad file descriptor");
g_test_expect_message ("cockpit-protocol", G_LOG_LEVEL_WARNING, "*Bad file descriptor");
transport = cockpit_pipe_transport_new_fds ("test", 1000, fds[0]);
g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
WAIT_UNTIL (problem != NULL);
g_assert_cmpstr (problem, ==, "internal-error");
g_free (problem);
cockpit_assert_expected ();
g_object_unref (transport);
close (fds[1]);
}
static void
test_write_error (void)
{
CockpitTransport *transport;
gchar *problem = NULL;
GBytes *sent;
int fds[2];
if (pipe(fds) < 0)
g_assert_not_reached ();
g_assert (write (1000, "1", 1) < 0);
g_test_expect_message ("cockpit-protocol", G_LOG_LEVEL_WARNING, "*Bad file descriptor");
g_test_expect_message ("cockpit-protocol", G_LOG_LEVEL_WARNING, "*Bad file descriptor");
transport = cockpit_pipe_transport_new_fds ("test", fds[0], 1000);
sent = g_bytes_new ("test", 4);
cockpit_transport_send (transport, "3333", sent);
g_bytes_unref (sent);
g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
WAIT_UNTIL (problem != NULL);
g_assert_cmpstr (problem, ==, "internal-error");
g_free (problem);
close (fds[0]);
close (fds[1]);
g_object_unref (transport);
cockpit_assert_expected ();
}
static void
test_read_combined (void)
{
CockpitTransport *transport;
struct iovec iov[4];
gint state = 0;
gint fds[2];
gint out;
if (pipe(fds) < 0)
g_assert_not_reached ();
out = dup (2);
g_assert (out >= 0);
transport = cockpit_pipe_transport_new_fds ("test", fds[0], out);
g_signal_connect (transport, "recv", G_CALLBACK (on_recv_multiple), &state);
iov[0].iov_base = "5\n";
iov[0].iov_len = 2;
iov[1].iov_base = "9\none";
iov[1].iov_len = 5;
iov[2].iov_base = "5\n";
iov[2].iov_len = 2;
iov[3].iov_base = "9\ntwo";
iov[3].iov_len = 5;
g_assert_cmpint (writev (fds[1], iov, 4), ==, 14);
WAIT_UNTIL (state == 2);
close (fds[1]);
g_object_unref (transport);
}
static void
test_read_truncated (void)
{
CockpitTransport *transport;
gchar *problem = NULL;
gint fds[2];
gint out;
if (pipe(fds) < 0)
g_assert_not_reached ();
out = dup (2);
g_assert (out >= 0);
transport = cockpit_pipe_transport_new_fds ("test", fds[0], out);
g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
g_assert_cmpint (write (fds[1], "5", 1), ==, 1);
g_assert_cmpint (close (fds[1]), ==, 0);
WAIT_UNTIL (problem != NULL);
g_assert_cmpstr (problem, ==, "disconnected");
g_free (problem);
g_object_unref (transport);
cockpit_assert_expected ();
}
static void
test_incorrect_protocol (void)
{
CockpitTransport *transport;
gchar *problem = NULL;
gint fds[2];
gint out;
if (pipe(fds) < 0)
g_assert_not_reached ();
out = dup (2);
g_assert (out >= 0);
g_test_expect_message ("cockpit-protocol", G_LOG_LEVEL_WARNING, "*received invalid length prefix");
transport = cockpit_pipe_transport_new_fds ("test", fds[0], out);
g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
g_assert_cmpint (write (fds[1], "X", 1), ==, 1);
g_assert_cmpint (close (fds[1]), ==, 0);
WAIT_UNTIL (problem != NULL);
g_assert_cmpstr (problem, ==, "protocol-error");
g_free (problem);
g_object_unref (transport);
cockpit_assert_expected ();
}
static void
test_parse_frame (void)
{
GBytes *message;
GBytes *payload;
gchar *channel;
message = g_bytes_new_static ("134\ntest", 8);
payload = cockpit_transport_parse_frame (message, &channel);
g_assert (payload != NULL);
g_assert_cmpstr (g_bytes_get_data (payload, NULL), ==, "test");
g_assert_cmpstr (channel, ==, "134");
g_bytes_unref (payload);
g_bytes_unref (message);
g_free (channel);
}
static void
test_parse_frame_bad (void)
{
gchar *channel = NULL;
GBytes *message;
GBytes *payload;
g_test_expect_message ("cockpit-protocol", G_LOG_LEVEL_MESSAGE, "*invalid channel prefix");
message = g_bytes_new_static ("b\x00y\ntest", 8);
payload = cockpit_transport_parse_frame (message, &channel);
g_assert (payload == NULL);
g_bytes_unref (message);
g_free (channel);
cockpit_assert_expected ();
g_test_expect_message ("cockpit-protocol", G_LOG_LEVEL_MESSAGE, "*invalid message without channel prefix");
channel = NULL;
message = g_bytes_new_static ("test", 4);
payload = cockpit_transport_parse_frame (message, &channel);
g_assert (payload == NULL);
g_bytes_unref (message);
g_free (channel);
cockpit_assert_expected ();
}
static void
test_parse_command (void)
{
const gchar *input = "{ \"command\": \"test\", \"channel\": \"66\", \"opt\": \"one\" }";
GBytes *message;
const gchar *channel;
const gchar *command;
JsonObject *options;
gboolean ret;
message = g_bytes_new_static (input, strlen (input));
ret = cockpit_transport_parse_command (message, &command, &channel, &options);
g_bytes_unref (message);
g_assert (ret == TRUE);
g_assert_cmpstr (command, ==, "test");
g_assert_cmpstr (channel, ==, "66");
g_assert_cmpstr (json_object_get_string_member (options, "opt"), ==, "one");
json_object_unref (options);
}
static void
test_parse_command_no_channel (void)
{
const gchar *input = "{ \"command\": \"test\", \"opt\": \"one\" }";
GBytes *message;
const gchar *channel;
const gchar *command;
JsonObject *options;
gboolean ret;
message = g_bytes_new_static (input, strlen (input));
ret = cockpit_transport_parse_command (message, &command, &channel, &options);
g_bytes_unref (message);
g_assert (ret == TRUE);
g_assert_cmpstr (command, ==, "test");
g_assert_cmpstr (channel, ==, NULL);
g_assert_cmpstr (json_object_get_string_member (options, "opt"), ==, "one");
json_object_unref (options);
}
static void
test_parse_command_nulls (void)
{
const gchar *input = "{ \"command\": \"test\", \"opt\": \"one\" }";
GBytes *message;
JsonObject *options;
gboolean ret;
message = g_bytes_new_static (input, strlen (input));
ret = cockpit_transport_parse_command (message, NULL, NULL, &options);
g_bytes_unref (message);
g_assert (ret == TRUE);
g_assert_cmpstr (json_object_get_string_member (options, "opt"), ==, "one");
json_object_unref (options);
}
struct {
const char *name;
const char *json;
} bad_command_payloads[] = {
{ "no-command", "{ \"no-command\": \"test\" }", },
{ "empty-command", "{ \"command\": \"\" }", },
{ "invalid-json", "{ xxxxxxxxxxxxxxxxxxxxx", },
{ "not-an-object", "55", },
{ "number-channel", "{ \"command\": \"test\", \"channel\": 0 }", },
{ "empty-channel", "{ \"command\": \"test\", \"channel\": \"\" }", },
{ "newline-channel", "{ \"command\": \"test\", \"channel\": \"blah\nline\" }", },
};
static void
test_parse_command_bad (gconstpointer input)
{
GBytes *message;
const gchar *channel;
const gchar *command;
JsonObject *options;
gboolean ret;
g_test_expect_message ("cockpit-protocol", G_LOG_LEVEL_WARNING, "*");
message = g_bytes_new_static (input, strlen (input));
ret = cockpit_transport_parse_command (message, &command, &channel, &options);
g_bytes_unref (message);
g_assert (ret == FALSE);
cockpit_assert_expected ();
}
int
main (int argc,
char *argv[])
{
gint i;
cockpit_test_init (&argc, &argv);
g_test_add_func ("/transport/parse-frame/ok", test_parse_frame);
g_test_add_func ("/transport/parse-frame/bad", test_parse_frame_bad);
g_test_add_func ("/transport/parse-command/normal", test_parse_command);
g_test_add_func ("/transport/parse-command/no-channel", test_parse_command_no_channel);
g_test_add_func ("/transport/parse-command/nulls", test_parse_command_nulls);
for (i = 0; i < G_N_ELEMENTS (bad_command_payloads); i++)
{
gchar *name = g_strdup_printf ("/transport/parse-command/%s", bad_command_payloads[i].name);
g_test_add_data_func (name, bad_command_payloads[i].json, test_parse_command_bad);
g_free (name);
}
g_test_add ("/transport/properties", TestCase, NULL,
setup_no_child, test_properties, teardown_transport);
g_test_add ("/transport/echo-message/child", TestCase,
BUILDDIR "/mock-echo", setup_with_child,
test_echo_and_close, teardown_transport);
g_test_add ("/transport/echo-message/no-child", TestCase,
NULL, setup_no_child,
test_echo_and_close, teardown_transport);
g_test_add ("/transport/echo-queue/child", TestCase,
BUILDDIR "/mock-echo", setup_with_child,
test_echo_queue, teardown_transport);
g_test_add ("/transport/echo-queue/no-child", TestCase,
NULL, setup_no_child,
test_echo_queue, teardown_transport);
g_test_add ("/transport/echo-large/child", TestCase,
"cat", setup_with_child,
test_echo_large, teardown_transport);
g_test_add ("/transport/echo-large/no-child", TestCase,
NULL, setup_no_child,
test_echo_large, teardown_transport);
g_test_add ("/transport/close-problem/child", TestCase,
BUILDDIR "/mock-echo", setup_with_child,
test_close_problem, teardown_transport);
g_test_add ("/transport/close-problem/no-child", TestCase,
NULL, setup_no_child,
test_close_problem, teardown_transport);
g_test_add ("/transport/terminate-problem", TestCase,
BUILDDIR "/mock-echo", setup_with_child,
test_terminate_problem, teardown_transport);
g_test_add ("/transport/exception-problem", TestCase,
SRCDIR "/src/ws/mock-pipes/someprogram", setup_with_child,
test_exception_problem, teardown_transport);
g_test_add ("/transport/nocockpit-problem", TestCase,
SRCDIR "/src/ws/mock-pipes/cockpit-session", setup_with_child,
test_nocockpit_problem, teardown_transport);
g_test_add_func ("/transport/ping/pong", test_ping_pong);
g_test_add_func ("/transport/ping/channel", test_ping_channel);
g_test_add_func ("/transport/read-error", test_read_error);
g_test_add_func ("/transport/write-error", test_write_error);
g_test_add_func ("/transport/read-combined", test_read_combined);
g_test_add_func ("/transport/read-truncated", test_read_truncated);
g_test_add_func ("/transport/read-incorrect", test_incorrect_protocol);
return g_test_run ();
}