<commit_before>
/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2013 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "cockpitpipechannel.h"

#include "mock-transport.h"

#include "common/cockpitjson.h"
#include "common/cockpittest.h"

#include <json-glib/json-glib.h>

#include <glib/gstdio.h>
#include <gio/gunixsocketaddress.h>

#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

/* -----------------------------------------------------------------------------
 * Test
 */

typedef struct {
  GSocket *listen_sock;
  GSource *listen_source;
  GSocket *conn_sock;
  GSource *conn_source;
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *channel_problem;
  const gchar *unix_path;
  gchar *temp_file;
} TestCase;

static gboolean
on_socket_input (GSocket *socket,
                 GIOCondition cond,
                 gpointer user_data)
{
  gchar buffer[1024];
  GError *error = NULL;
  gssize ret, wret;

  ret = g_socket_receive (socket, buffer, sizeof (buffer), NULL, &error);
  g_assert_no_error (error);

  if (ret == 0)
    {
      g_socket_shutdown (socket, FALSE, TRUE, &error);
      g_assert_no_error (error);
      return FALSE;
    }

  g_assert (ret > 0);
  wret = g_socket_send (socket, buffer, ret, NULL, &error);
  g_assert_no_error (error);
  g_assert (wret == ret);
  return TRUE;
}

static gboolean
on_socket_connection (GSocket *socket,
                      GIOCondition cond,
                      gpointer user_data)
{
  TestCase *tc = user_data;
  GError *error = NULL;

  g_assert (tc->conn_source == NULL);
  tc->conn_sock = g_socket_accept (tc->listen_sock, NULL, &error);
  g_assert_no_error (error);

  tc->conn_source = g_socket_create_source (tc->conn_sock, G_IO_IN, NULL);
  g_source_set_callback (tc->conn_source, (GSourceFunc)on_socket_input, tc, NULL);
  g_source_attach (tc->conn_source, NULL);

  /* Only one connection */
  return FALSE;
}

static void
setup (TestCase *tc,
       gconstpointer data)
{
  GSocketAddress *address;
  GError *error = NULL;

  tc->unix_path = data;
  if (tc->unix_path == NULL)
    {
      tc->unix_path = tc->temp_file = g_strdup ("/tmp/cockpit-test-XXXXXX.sock");
      g_assert (close (g_mkstemp (tc->temp_file)) == 0);
      g_assert (g_unlink (tc->temp_file) == 0);
    }

  address = g_unix_socket_address_new (tc->unix_path);

  tc->listen_sock = g_socket_new (G_SOCKET_FAMILY_UNIX, G_SOCKET_TYPE_STREAM,
                                  G_SOCKET_PROTOCOL_DEFAULT, &error);
  g_assert_no_error (error);

  g_socket_bind (tc->listen_sock, address, TRUE, &error);
  g_assert_no_error (error);

  g_socket_listen (tc->listen_sock, &error);
  g_assert_no_error (error);

  tc->listen_source = g_socket_create_source (tc->listen_sock, G_IO_IN, NULL);
  g_source_set_callback (tc->listen_source, (GSourceFunc)on_socket_connection, tc, NULL);
  g_source_attach (tc->listen_source, NULL);

  g_object_unref (address);

  tc->transport = g_object_new (mock_transport_get_type (), NULL);
}

static void
on_closed_get_problem (CockpitChannel *channel,
                       const gchar *problem,
                       gpointer user_data)
{
  gchar **retval = user_data;
  g_assert (retval != NULL && *retval == NULL);
  *retval = g_strdup (problem ? problem : "");
}

static void
setup_channel (TestCase *tc,
               gconstpointer data)
{
  setup (tc, data);
  tc->channel = cockpit_pipe_channel_open (COCKPIT_TRANSPORT (tc->transport), "548", tc->unix_path);
  g_signal_connect (tc->channel, "closed", G_CALLBACK (on_closed_get_problem), &tc->channel_problem);
}

static void
teardown (TestCase *tc,
          gconstpointer data)
{
  if (tc->conn_source)
    {
      g_source_destroy (tc->conn_source);
      g_source_unref (tc->conn_source);
    }
  if (tc->listen_source)
    {
      g_source_destroy (tc->listen_source);
      g_source_unref (tc->listen_source);
    }
  g_clear_object (&tc->listen_sock);
  g_clear_object (&tc->conn_sock);

  g_unlink (tc->unix_path);
  g_free (tc->temp_file);

  g_object_unref (tc->transport);

  if (tc->channel)
    {
      g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
      g_object_unref (tc->channel);
      g_assert (tc->channel == NULL);
    }

  g_free (tc->channel_problem);

  cockpit_assert_expected ();
}

static void
expect_control_message (JsonObject *options,
                        const gchar *command,
                        const gchar *expected_channel,
                        ...) G_GNUC_NULL_TERMINATED;

static void
expect_control_message (JsonObject *options,
                        const gchar *expected_command,
                        const gchar *expected_channel,
                        ...)
{
  const gchar *expect_option;
  const gchar *expect_value;
  const gchar *value;
  JsonNode *node;
  va_list va;

  g_assert (options != NULL);
  g_assert_cmpstr (json_object_get_string_member (options, "command"), ==, expected_command);
  g_assert_cmpstr (json_object_get_string_member (options, "channel"), ==, expected_channel);

  va_start (va, expected_channel);
  for (;;) {
      expect_option = va_arg (va, const gchar *);
      if (!expect_option)
        break;
      expect_value = va_arg (va, const gchar *);

      value = NULL;
      node = json_object_get_member (options, expect_option);
      if (node && JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) == G_TYPE_STRING)
        value = json_node_get_string (node);

      g_assert_cmpstr (value, ==, expect_value);
  }
  va_end (va);
}


static void
test_echo (TestCase *tc,
           gconstpointer unused)
{
  GBytes *payload;
  GBytes *sent;

  payload = g_bytes_new ("Marmalaade!", 11);
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);

  while (mock_transport_count_sent (tc->transport) < 2)
    g_main_context_iteration (NULL, TRUE);

  sent = mock_transport_pop_channel (tc->transport, "548");
  cockpit_assert_bytes_eq (sent, "Marmalaade!", 11);
  g_bytes_unref (payload);
}

static void
test_shutdown (TestCase *tc,
               gconstpointer unused)
{
  GError *error = NULL;
  JsonObject *sent;

  /* Wait until the socket has opened */
  while (tc->conn_sock == NULL)
    g_main_context_iteration (NULL, TRUE);

  /*
   * Close down the write end of the socket (what
   * CockpitTextStream is reading from)
   */
  g_socket_shutdown (tc->conn_sock, FALSE, TRUE, &error);
  g_assert_no_error (error);

  while (tc->channel_problem == NULL)
    g_main_context_iteration (NULL, TRUE);

  g_assert_cmpstr (tc->channel_problem, ==, "");
  sent = mock_transport_pop_control (tc->transport);
  expect_control_message (sent, "ready", "548", NULL);
  sent = mock_transport_pop_control (tc->transport);
  expect_control_message (sent, "done", "548", NULL);

  sent = mock_transport_pop_control (tc->transport);
  expect_control_message (sent, "close", "548", "problem", NULL, NULL);
}

static void
test_close_normal (TestCase *tc,
                   gconstpointer unused)
{
  GBytes *payload;
  GBytes *sent;
  JsonObject *control;

  /* Wait until the socket has opened */
  while (tc->conn_sock == NULL)
    g_main_context_iteration (NULL, TRUE);

  payload = g_bytes_new ("Marmalaade!", 11);
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);
  cockpit_channel_close (tc->channel, NULL);

  /* Wait until channel closes */
  while (tc->channel_problem == NULL)
    g_main_context_iteration (NULL, TRUE);

  /* Should have sent payload and control */
  g_assert_cmpstr (tc->channel_problem, ==, "");
  sent = mock_transport_pop_channel (tc->transport, "548");
  g_assert (sent != NULL);
  g_assert (g_bytes_equal (sent, payload));
  g_bytes_unref (payload);

  control = mock_transport_pop_control (tc->transport);
  expect_control_message (control, "ready", "548", NULL);
  control = mock_transport_pop_control (tc->transport);
  expect_control_message (control, "done", "548", NULL);

  control = mock_transport_pop_control (tc->transport);
  expect_control_message (control, "close", "548", "problem", NULL, NULL);
}

static void
test_close_problem (TestCase *tc,
                    gconstpointer unused)
{
  GBytes *sent;

  /* Wait until the socket has opened */
  while (tc->conn_sock == NULL)
    g_main_context_iteration (NULL, TRUE);

  sent = g_bytes_new ("Marmalaade!", 11);
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", sent);
  g_bytes_unref (sent);
  cockpit_channel_close (tc->channel, "boooyah");

  /* Wait until channel closes */
  while (tc->channel_problem == NULL)
    g_main_context_iteration (NULL, TRUE);

  /* Should have sent no payload and control */
  g_assert_cmpstr (tc->channel_problem, ==, "boooyah");
  g_assert (mock_transport_pop_channel (tc->transport, "548") == NULL);
  expect_control_message (mock_transport_pop_control (tc->transport), "ready", "548", NULL);
  expect_control_message (mock_transport_pop_control (tc->transport),
                          "close", "548", "problem", "boooyah", NULL);
}

static void
test_spawn_simple (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  GBytes *sent;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();
  array = json_array_new ();
  json_array_add_string_element (array, "/bin/cat");
  json_object_set_array_member (options, "spawn", array);
  json_object_set_string_member (options, "payload", "stream");

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  while (g_main_context_iteration (NULL, FALSE));
  json_object_unref (options);

  sent = g_bytes_new ("Marmalaade!", 11);
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
  cockpit_channel_close (channel, NULL);

  while (mock_transport_count_sent (transport) < 2)
    g_main_context_iteration (NULL, TRUE);
  g_assert (g_bytes_equal (sent, mock_transport_pop_channel (transport, "548")));
  g_bytes_unref (sent);

  while (!problem)
    g_main_context_iteration (NULL, TRUE);

  g_assert_cmpstr (problem, ==, "");
  g_object_unref (channel);

  g_free (problem);
  g_object_unref (transport);
}

static void
test_spawn_environ (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  GString *string;
  gconstpointer data;
  gsize len;
  GBytes *sent;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();

  array = json_array_new ();
  json_array_add_string_element (array, "/bin/sh");
  json_array_add_string_element (array, "-c");
  json_array_add_string_element (array, "set");
  json_object_set_array_member (options, "spawn", array);

  array = json_array_new ();
  json_array_add_string_element (array, "ENVIRON=Marmalaade");
  json_object_set_array_member (options, "environ", array);

  json_object_set_string_member (options, "payload", "stream");

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  json_object_unref (options);

  string = g_string_new ("");
  while (!problem)
    {
      g_main_context_iteration (NULL, TRUE);
      sent = mock_transport_pop_channel (transport, "548");
      if (sent)
        {
          data = g_bytes_get_data (sent, &len);
          g_string_append_len (string, data, len);
        }
    }

  g_assert_cmpstr (problem, ==, "");
  g_free (problem);

  cockpit_assert_strmatch (string->str, "*ENVIRON=*Marmalaade*");
  g_string_free (string, TRUE);

  g_object_unref (channel);
  g_object_unref (transport);
}

static void
test_spawn_status (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  JsonObject *control;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();

  array = json_array_new ();
  json_array_add_string_element (array, "/bin/sh");
  json_array_add_string_element (array, "-c");
  json_array_add_string_element (array, "exit 5");
  json_object_set_array_member (options, "spawn", array);

  json_object_set_string_member (options, "payload", "stream");

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  json_object_unref (options);

  while (!problem)
    g_main_context_iteration (NULL, TRUE);

  control = mock_transport_pop_control (transport);
  expect_control_message (control, "ready", "548", NULL);
  control = mock_transport_pop_control (transport);
  expect_control_message (control, "done", "548", NULL);

  control = mock_transport_pop_control (transport);
  expect_control_message (control, "close", "548", "problem", NULL, NULL);
  g_assert_cmpint (json_object_get_int_member (control, "exit-status"), ==, 5);

  g_free (problem);

  g_object_unref (channel);
  g_object_unref (transport);
}

static void
test_spawn_signal (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  JsonObject *control;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();

  array = json_array_new ();
  json_array_add_string_element (array, "/bin/sh");
  json_array_add_string_element (array, "-c");
  json_array_add_string_element (array, "kill $$");
  json_object_set_array_member (options, "spawn", array);

  json_object_set_string_member (options, "payload", "stream");

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  json_object_unref (options);

  while (!problem)
    g_main_context_iteration (NULL, TRUE);

  control = mock_transport_pop_control (transport);
  expect_control_message (control, "ready", "548", NULL);
  control = mock_transport_pop_control (transport);
  expect_control_message (control, "done", "548", NULL);

  control = mock_transport_pop_control (transport);
  cockpit_assert_json_eq (control, "{ \"command\": \"close\", \"channel\": \"548\","
                                   "  \"exit-signal\": \"TERM\"}");

  g_free (problem);

  g_object_unref (channel);
  g_object_unref (transport);
}

static void
test_spawn_pty (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  GBytes *sent;
  GString *received;
  gconstpointer data;
  gsize len;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();
  array = json_array_new ();
  json_array_add_string_element (array, "/bin/bash");
  json_array_add_string_element (array, "-i");
  json_object_set_array_member (options, "spawn", array);
  json_object_set_string_member (options, "payload", "stream");
  json_object_set_boolean_member (options, "pty", TRUE);

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  json_object_unref (options);

  sent = g_bytes_new ("echo booyah\nexit\n", 17);
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
  g_bytes_unref (sent);

  received = g_string_new ("");
  while (!problem)
    {
      g_main_context_iteration (NULL, TRUE);
      sent = mock_transport_pop_channel (transport, "548");
      if (sent)
        {
          data = g_bytes_get_data (sent, &len);
          g_string_append_len (received, data, len);
        }
    }

  cockpit_assert_strmatch (received->str, "*booyah*");
  g_string_free (received, TRUE);

  g_assert_cmpstr (problem, ==, "");
  g_object_unref (channel);

  g_free (problem);
  g_object_unref (transport);
}

/* Create GBytes with the contents of @s, without terminating \0 */
static GBytes *
bytes_from_string (const gchar *s)
{
  gsize len;

  len = strlen (s);
  return g_bytes_new (s, len);
}

static void
test_spawn_pty_resize (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  JsonObject *window;
  GBytes *sent;
  GString *received;
  gconstpointer data;
  gsize len;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();
  array = json_array_new ();
  json_array_add_string_element (array, "/bin/bash");
  json_array_add_string_element (array, "-i");
  json_object_set_array_member (options, "spawn", array);
  json_object_set_string_member (options, "payload", "stream");
  json_object_set_boolean_member (options, "pty", TRUE);
  window = json_object_new ();
  json_object_set_int_member (window, "rows", 1234);
  json_object_set_int_member (window, "cols", 4567);
  json_object_set_object_member (options, "window", window);

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  json_object_unref (options);

  sent = bytes_from_string ("echo -e \"\\x7b$COLUMNS $LINES\\x7d\"\n");
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
  g_bytes_unref (sent);

  received = g_string_new ("");
  while (!problem)
    {
      g_main_context_iteration (NULL, TRUE);
      sent = mock_transport_pop_channel (transport, "548");
      if (sent)<commit_msg>bridge: enable checkwinsize in pty resize tests<commit_after>
/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2013 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "cockpitpipechannel.h"

#include "mock-transport.h"

#include "common/cockpitjson.h"
#include "common/cockpittest.h"

#include <json-glib/json-glib.h>

#include <glib/gstdio.h>
#include <gio/gunixsocketaddress.h>

#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

/* -----------------------------------------------------------------------------
 * Test
 */

typedef struct {
  GSocket *listen_sock;
  GSource *listen_source;
  GSocket *conn_sock;
  GSource *conn_source;
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *channel_problem;
  const gchar *unix_path;
  gchar *temp_file;
} TestCase;

static gboolean
on_socket_input (GSocket *socket,
                 GIOCondition cond,
                 gpointer user_data)
{
  gchar buffer[1024];
  GError *error = NULL;
  gssize ret, wret;

  ret = g_socket_receive (socket, buffer, sizeof (buffer), NULL, &error);
  g_assert_no_error (error);

  if (ret == 0)
    {
      g_socket_shutdown (socket, FALSE, TRUE, &error);
      g_assert_no_error (error);
      return FALSE;
    }

  g_assert (ret > 0);
  wret = g_socket_send (socket, buffer, ret, NULL, &error);
  g_assert_no_error (error);
  g_assert (wret == ret);
  return TRUE;
}

static gboolean
on_socket_connection (GSocket *socket,
                      GIOCondition cond,
                      gpointer user_data)
{
  TestCase *tc = user_data;
  GError *error = NULL;

  g_assert (tc->conn_source == NULL);
  tc->conn_sock = g_socket_accept (tc->listen_sock, NULL, &error);
  g_assert_no_error (error);

  tc->conn_source = g_socket_create_source (tc->conn_sock, G_IO_IN, NULL);
  g_source_set_callback (tc->conn_source, (GSourceFunc)on_socket_input, tc, NULL);
  g_source_attach (tc->conn_source, NULL);

  /* Only one connection */
  return FALSE;
}

static void
setup (TestCase *tc,
       gconstpointer data)
{
  GSocketAddress *address;
  GError *error = NULL;

  tc->unix_path = data;
  if (tc->unix_path == NULL)
    {
      tc->unix_path = tc->temp_file = g_strdup ("/tmp/cockpit-test-XXXXXX.sock");
      g_assert (close (g_mkstemp (tc->temp_file)) == 0);
      g_assert (g_unlink (tc->temp_file) == 0);
    }

  address = g_unix_socket_address_new (tc->unix_path);

  tc->listen_sock = g_socket_new (G_SOCKET_FAMILY_UNIX, G_SOCKET_TYPE_STREAM,
                                  G_SOCKET_PROTOCOL_DEFAULT, &error);
  g_assert_no_error (error);

  g_socket_bind (tc->listen_sock, address, TRUE, &error);
  g_assert_no_error (error);

  g_socket_listen (tc->listen_sock, &error);
  g_assert_no_error (error);

  tc->listen_source = g_socket_create_source (tc->listen_sock, G_IO_IN, NULL);
  g_source_set_callback (tc->listen_source, (GSourceFunc)on_socket_connection, tc, NULL);
  g_source_attach (tc->listen_source, NULL);

  g_object_unref (address);

  tc->transport = g_object_new (mock_transport_get_type (), NULL);
}

static void
on_closed_get_problem (CockpitChannel *channel,
                       const gchar *problem,
                       gpointer user_data)
{
  gchar **retval = user_data;
  g_assert (retval != NULL && *retval == NULL);
  *retval = g_strdup (problem ? problem : "");
}

static void
setup_channel (TestCase *tc,
               gconstpointer data)
{
  setup (tc, data);
  tc->channel = cockpit_pipe_channel_open (COCKPIT_TRANSPORT (tc->transport), "548", tc->unix_path);
  g_signal_connect (tc->channel, "closed", G_CALLBACK (on_closed_get_problem), &tc->channel_problem);
}

static void
teardown (TestCase *tc,
          gconstpointer data)
{
  if (tc->conn_source)
    {
      g_source_destroy (tc->conn_source);
      g_source_unref (tc->conn_source);
    }
  if (tc->listen_source)
    {
      g_source_destroy (tc->listen_source);
      g_source_unref (tc->listen_source);
    }
  g_clear_object (&tc->listen_sock);
  g_clear_object (&tc->conn_sock);

  g_unlink (tc->unix_path);
  g_free (tc->temp_file);

  g_object_unref (tc->transport);

  if (tc->channel)
    {
      g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
      g_object_unref (tc->channel);
      g_assert (tc->channel == NULL);
    }

  g_free (tc->channel_problem);

  cockpit_assert_expected ();
}

static void
expect_control_message (JsonObject *options,
                        const gchar *command,
                        const gchar *expected_channel,
                        ...) G_GNUC_NULL_TERMINATED;

static void
expect_control_message (JsonObject *options,
                        const gchar *expected_command,
                        const gchar *expected_channel,
                        ...)
{
  const gchar *expect_option;
  const gchar *expect_value;
  const gchar *value;
  JsonNode *node;
  va_list va;

  g_assert (options != NULL);
  g_assert_cmpstr (json_object_get_string_member (options, "command"), ==, expected_command);
  g_assert_cmpstr (json_object_get_string_member (options, "channel"), ==, expected_channel);

  va_start (va, expected_channel);
  for (;;) {
      expect_option = va_arg (va, const gchar *);
      if (!expect_option)
        break;
      expect_value = va_arg (va, const gchar *);

      value = NULL;
      node = json_object_get_member (options, expect_option);
      if (node && JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) == G_TYPE_STRING)
        value = json_node_get_string (node);

      g_assert_cmpstr (value, ==, expect_value);
  }
  va_end (va);
}


static void
test_echo (TestCase *tc,
           gconstpointer unused)
{
  GBytes *payload;
  GBytes *sent;

  payload = g_bytes_new ("Marmalaade!", 11);
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);

  while (mock_transport_count_sent (tc->transport) < 2)
    g_main_context_iteration (NULL, TRUE);

  sent = mock_transport_pop_channel (tc->transport, "548");
  cockpit_assert_bytes_eq (sent, "Marmalaade!", 11);
  g_bytes_unref (payload);
}

static void
test_shutdown (TestCase *tc,
               gconstpointer unused)
{
  GError *error = NULL;
  JsonObject *sent;

  /* Wait until the socket has opened */
  while (tc->conn_sock == NULL)
    g_main_context_iteration (NULL, TRUE);

  /*
   * Close down the write end of the socket (what
   * CockpitTextStream is reading from)
   */
  g_socket_shutdown (tc->conn_sock, FALSE, TRUE, &error);
  g_assert_no_error (error);

  while (tc->channel_problem == NULL)
    g_main_context_iteration (NULL, TRUE);

  g_assert_cmpstr (tc->channel_problem, ==, "");
  sent = mock_transport_pop_control (tc->transport);
  expect_control_message (sent, "ready", "548", NULL);
  sent = mock_transport_pop_control (tc->transport);
  expect_control_message (sent, "done", "548", NULL);

  sent = mock_transport_pop_control (tc->transport);
  expect_control_message (sent, "close", "548", "problem", NULL, NULL);
}

static void
test_close_normal (TestCase *tc,
                   gconstpointer unused)
{
  GBytes *payload;
  GBytes *sent;
  JsonObject *control;

  /* Wait until the socket has opened */
  while (tc->conn_sock == NULL)
    g_main_context_iteration (NULL, TRUE);

  payload = g_bytes_new ("Marmalaade!", 11);
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);
  cockpit_channel_close (tc->channel, NULL);

  /* Wait until channel closes */
  while (tc->channel_problem == NULL)
    g_main_context_iteration (NULL, TRUE);

  /* Should have sent payload and control */
  g_assert_cmpstr (tc->channel_problem, ==, "");
  sent = mock_transport_pop_channel (tc->transport, "548");
  g_assert (sent != NULL);
  g_assert (g_bytes_equal (sent, payload));
  g_bytes_unref (payload);

  control = mock_transport_pop_control (tc->transport);
  expect_control_message (control, "ready", "548", NULL);
  control = mock_transport_pop_control (tc->transport);
  expect_control_message (control, "done", "548", NULL);

  control = mock_transport_pop_control (tc->transport);
  expect_control_message (control, "close", "548", "problem", NULL, NULL);
}

static void
test_close_problem (TestCase *tc,
                    gconstpointer unused)
{
  GBytes *sent;

  /* Wait until the socket has opened */
  while (tc->conn_sock == NULL)
    g_main_context_iteration (NULL, TRUE);

  sent = g_bytes_new ("Marmalaade!", 11);
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", sent);
  g_bytes_unref (sent);
  cockpit_channel_close (tc->channel, "boooyah");

  /* Wait until channel closes */
  while (tc->channel_problem == NULL)
    g_main_context_iteration (NULL, TRUE);

  /* Should have sent no payload and control */
  g_assert_cmpstr (tc->channel_problem, ==, "boooyah");
  g_assert (mock_transport_pop_channel (tc->transport, "548") == NULL);
  expect_control_message (mock_transport_pop_control (tc->transport), "ready", "548", NULL);
  expect_control_message (mock_transport_pop_control (tc->transport),
                          "close", "548", "problem", "boooyah", NULL);
}

static void
test_spawn_simple (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  GBytes *sent;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();
  array = json_array_new ();
  json_array_add_string_element (array, "/bin/cat");
  json_object_set_array_member (options, "spawn", array);
  json_object_set_string_member (options, "payload", "stream");

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  while (g_main_context_iteration (NULL, FALSE));
  json_object_unref (options);

  sent = g_bytes_new ("Marmalaade!", 11);
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
  cockpit_channel_close (channel, NULL);

  while (mock_transport_count_sent (transport) < 2)
    g_main_context_iteration (NULL, TRUE);
  g_assert (g_bytes_equal (sent, mock_transport_pop_channel (transport, "548")));
  g_bytes_unref (sent);

  while (!problem)
    g_main_context_iteration (NULL, TRUE);

  g_assert_cmpstr (problem, ==, "");
  g_object_unref (channel);

  g_free (problem);
  g_object_unref (transport);
}

static void
test_spawn_environ (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  GString *string;
  gconstpointer data;
  gsize len;
  GBytes *sent;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();

  array = json_array_new ();
  json_array_add_string_element (array, "/bin/sh");
  json_array_add_string_element (array, "-c");
  json_array_add_string_element (array, "set");
  json_object_set_array_member (options, "spawn", array);

  array = json_array_new ();
  json_array_add_string_element (array, "ENVIRON=Marmalaade");
  json_object_set_array_member (options, "environ", array);

  json_object_set_string_member (options, "payload", "stream");

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  json_object_unref (options);

  string = g_string_new ("");
  while (!problem)
    {
      g_main_context_iteration (NULL, TRUE);
      sent = mock_transport_pop_channel (transport, "548");
      if (sent)
        {
          data = g_bytes_get_data (sent, &len);
          g_string_append_len (string, data, len);
        }
    }

  g_assert_cmpstr (problem, ==, "");
  g_free (problem);

  cockpit_assert_strmatch (string->str, "*ENVIRON=*Marmalaade*");
  g_string_free (string, TRUE);

  g_object_unref (channel);
  g_object_unref (transport);
}

static void
test_spawn_status (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  JsonObject *control;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();

  array = json_array_new ();
  json_array_add_string_element (array, "/bin/sh");
  json_array_add_string_element (array, "-c");
  json_array_add_string_element (array, "exit 5");
  json_object_set_array_member (options, "spawn", array);

  json_object_set_string_member (options, "payload", "stream");

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  json_object_unref (options);

  while (!problem)
    g_main_context_iteration (NULL, TRUE);

  control = mock_transport_pop_control (transport);
  expect_control_message (control, "ready", "548", NULL);
  control = mock_transport_pop_control (transport);
  expect_control_message (control, "done", "548", NULL);

  control = mock_transport_pop_control (transport);
  expect_control_message (control, "close", "548", "problem", NULL, NULL);
  g_assert_cmpint (json_object_get_int_member (control, "exit-status"), ==, 5);

  g_free (problem);

  g_object_unref (channel);
  g_object_unref (transport);
}

static void
test_spawn_signal (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  JsonObject *control;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();

  array = json_array_new ();
  json_array_add_string_element (array, "/bin/sh");
  json_array_add_string_element (array, "-c");
  json_array_add_string_element (array, "kill $$");
  json_object_set_array_member (options, "spawn", array);

  json_object_set_string_member (options, "payload", "stream");

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  json_object_unref (options);

  while (!problem)
    g_main_context_iteration (NULL, TRUE);

  control = mock_transport_pop_control (transport);
  expect_control_message (control, "ready", "548", NULL);
  control = mock_transport_pop_control (transport);
  expect_control_message (control, "done", "548", NULL);

  control = mock_transport_pop_control (transport);
  cockpit_assert_json_eq (control, "{ \"command\": \"close\", \"channel\": \"548\","
                                   "  \"exit-signal\": \"TERM\"}");

  g_free (problem);

  g_object_unref (channel);
  g_object_unref (transport);
}

static void
test_spawn_pty (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  GBytes *sent;
  GString *received;
  gconstpointer data;
  gsize len;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();
  array = json_array_new ();
  json_array_add_string_element (array, "/bin/bash");
  json_array_add_string_element (array, "-i");
  json_object_set_array_member (options, "spawn", array);
  json_object_set_string_member (options, "payload", "stream");
  json_object_set_boolean_member (options, "pty", TRUE);

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  json_object_unref (options);

  sent = g_bytes_new ("echo booyah\nexit\n", 17);
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
  g_bytes_unref (sent);

  received = g_string_new ("");
  while (!problem)
    {
      g_main_context_iteration (NULL, TRUE);
      sent = mock_transport_pop_channel (transport, "548");
      if (sent)
        {
          data = g_bytes_get_data (sent, &len);
          g_string_append_len (received, data, len);
        }
    }

  cockpit_assert_strmatch (received->str, "*booyah*");
  g_string_free (received, TRUE);

  g_assert_cmpstr (problem, ==, "");
  g_object_unref (channel);

  g_free (problem);
  g_object_unref (transport);
}

/* Create GBytes with the contents of @s, without terminating \0 */
static GBytes *
bytes_from_string (const gchar *s)
{
  gsize len;

  len = strlen (s);
  return g_bytes_new (s, len);
}

static void
test_spawn_pty_resize (void)
{
  MockTransport *transport;
  CockpitChannel *channel;
  gchar *problem = NULL;
  JsonObject *options;
  JsonArray *array;
  JsonObject *window;
  GBytes *sent;
  GString *received;
  gconstpointer data;
  gsize len;

  transport = g_object_new (mock_transport_get_type (), NULL);

  options = json_object_new ();
  array = json_array_new ();
  json_array_add_string_element (array, "/bin/bash");
  json_array_add_string_element (array, "-i");
  json_object_set_array_member (options, "spawn", array);
  json_object_set_string_member (options, "payload", "stream");
  json_object_set_boolean_member (options, "pty", TRUE);
  window = json_object_new ();
  json_object_set_int_member (window, "rows", 1234);
  json_object_set_int_member (window, "cols", 4567);
  json_object_set_object_member (options, "window", window);

  channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
                          "options", options,
                          "id", "548",
                          "transport", transport,
                          NULL);
  g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
  json_object_unref (options);

  /*
   * Set shell option `checkwinsize`, which tells bash to update $LINES
   * and $COLUMNS after each command.
   */
  sent = bytes_from_string ("shopt -s checkwinsize\necho -e \"\\x7b$COLUMNS $LINES\\x7d\"\n");
  cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
  g_bytes_unref (sent);

  received = g_string_new ("");
  while (!problem)
    {
      g_main_context_iteration (NULL, TRUE);
      sent = mock_transport_pop_channel (transport, "548");
      if (sent)
{'fraction_non_alphanumeric': 0.1237469485275022, 'fraction_numerical': 0.007427413909520594, 'mean_word_length': 3.5700213624495607, 'pattern_counts': {'":': 6, '<': 23, '<?xml version=': 0, '>': 167, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4644799', 'n_tokens_mistral': 13198, 'n_tokens_neox': 12650, 'n_words': 2912}