/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2014 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 "cockpitwebinject.h"
#include "cockpitwebresponse.h"
#include "cockpitwebserver.h"

#include "testlib/cockpittest.h"

#include "websocket/websocket.h"

#include <glib/gstdio.h>

#include <stdlib.h>
#include <string.h>

/* headers that are present in every request */
#define STATIC_HEADERS "X-DNS-Prefetch-Control: off\r\nReferrer-Policy: no-referrer\r\nX-Content-Type-Options: nosniff\r\nCross-Origin-Resource-Policy: same-origin\r\nX-Frame-Options: sameorigin\r\n\r\n"
static gchar *srcdir;

typedef struct {
    CockpitWebResponse *response;
    GOutputStream *output;
    gchar *scratch;
    gboolean response_done;
    gulong sig_done;
} TestCase;

typedef struct {
    const gchar *path;
    const gchar *header;
    const gchar *value;
    const gchar *method;
    const gchar *expected_content_type;
    CockpitCacheType cache;
    gboolean for_tls_proxy;
} TestFixture;

static void
on_response_done (CockpitWebResponse *response,
                  gboolean reusable,
                  gpointer user_data)
{
  gboolean *response_done = user_data;
  g_assert (response_done != NULL);
  g_assert (*response_done == FALSE);
  *response_done = TRUE;
}

static void
setup (TestCase *tc,
       gconstpointer data)
{
  const TestFixture *fixture = data;
  const gchar *path = NULL;
  GHashTable *headers = NULL;
  GInputStream *input;
  GIOStream *io;

  if (fixture)
    path = fixture->path;

  input = g_memory_input_stream_new ();
  tc->output = g_memory_output_stream_new (NULL, 0, g_realloc, g_free);
  io = g_simple_io_stream_new (input, tc->output);
  g_object_unref (input);

  if (fixture && fixture->header)
    {
      headers = cockpit_web_server_new_table ();
      g_hash_table_insert (headers, g_strdup (fixture->header), g_strdup (fixture->value));
    }

  tc->response = cockpit_web_response_new (io, path, path, headers,
                                           (fixture && fixture->method) ? fixture->method : "GET",
                                           (fixture && fixture->for_tls_proxy) ? "https" : "http");

  if (headers)
    g_hash_table_unref (headers);
  g_object_unref (io);

  tc->sig_done = g_signal_connect (tc->response, "done",
                                   G_CALLBACK (on_response_done),
                                   &tc->response_done);
}

static void
teardown (TestCase *tc,
          gconstpointer data)
{
  while (g_main_context_iteration (NULL, FALSE));
  g_assert (tc->response_done);

  g_signal_handler_disconnect (tc->response, tc->sig_done);
  g_clear_object (&tc->output);
  g_clear_object (&tc->response);
  g_free (tc->scratch);
}

static const gchar *
output_as_string (TestCase *tc)
{
  while (!tc->response_done)
    g_main_context_iteration (NULL, TRUE);

  g_free (tc->scratch);
  tc->scratch = g_strndup (g_memory_output_stream_get_data (G_MEMORY_OUTPUT_STREAM (tc->output)),
                           g_memory_output_stream_get_data_size (G_MEMORY_OUTPUT_STREAM (tc->output)));
  return tc->scratch;
}

static void
test_return_content (TestCase *tc,
                     gconstpointer data)
{
  const gchar *resp;
  GBytes *content;

  content = g_bytes_new_static ("the content", 11);
  cockpit_web_response_content (tc->response, NULL, content, NULL);
  g_bytes_unref (content);

  resp = output_as_string (tc);
  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n" STATIC_HEADERS "the content");
}

static void
test_return_content_headers (TestCase *tc,
                             gconstpointer data)
{
  const gchar *resp;
  GHashTable *headers;
  GBytes *content;

  headers = cockpit_web_server_new_table ();
  g_hash_table_insert (headers, g_strdup ("My-header"), g_strdup ("my-value"));

  content = g_bytes_new_static ("the content", 11);
  cockpit_web_response_content (tc->response, headers, content, NULL);
  g_bytes_unref (content);
  g_hash_table_destroy (headers);

  resp = output_as_string (tc);
  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nMy-header: my-value\r\nContent-Length: 11\r\n" STATIC_HEADERS "the content");
}


static void
test_return_error (TestCase *tc,
                   gconstpointer data)
{
  const gchar *resp;

  cockpit_web_response_error (tc->response, 500, NULL, "Reason here: %s", "booyah");

  resp = output_as_string (tc);
  g_assert_cmpstr (resp, ==,
    "HTTP/1.1 500 Reason here: booyah\r\n"
    "Content-Type: text/html; charset=utf8\r\n"
    "Transfer-Encoding: chunked\r\n"
    STATIC_HEADERS
    "13\r\n<html><head><title>\r\n"
    "13\r\nReason here: booyah\r\n"
    "15\r\n</title></head><body>\r\n"
    "13\r\nReason here: booyah\r\n"
    "f\r\n</body></html>\n\r\n"
    "0\r\n\r\n");
}

static void
test_return_error_headers (TestCase *tc,
                           gconstpointer data)
{
  const gchar *resp;
  GHashTable *headers;

  headers = cockpit_web_server_new_table ();
  g_hash_table_insert (headers, g_strdup ("Header1"), g_strdup ("value1"));

  cockpit_web_response_error (tc->response, 500, headers, "Reason here: %s", "booyah");

  g_hash_table_destroy (headers);

  resp = output_as_string (tc);
  cockpit_assert_strmatch(resp,"HTTP/1.1 500 Reason here: booyah\r*\n"
    "Header1: value1\r*\n"
    "\r\n");
}

static void
test_return_gerror_headers (TestCase *tc,
                            gconstpointer data)
{
  const gchar *resp;
  GHashTable *headers;
  GError *error;

  headers = cockpit_web_server_new_table ();
  g_hash_table_insert (headers, g_strdup ("Header1"), g_strdup ("value1"));

  error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, "Reason here: %s", "booyah");
  cockpit_web_response_gerror (tc->response, headers, error);

  g_error_free (error);
  g_hash_table_destroy (headers);

  resp = output_as_string (tc);
  cockpit_assert_strmatch(resp,"HTTP/1.1 500 Reason here: booyah\r*\n"
    "Header1: value1\r*\n"
    "\r\n");
}

static void
test_file_not_found (TestCase *tc,
                     gconstpointer user_data)
{
  const gchar *roots[] = { srcdir, NULL };
  cockpit_web_response_file (tc->response, "/non-existent", roots);
  cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 404 Not Found*");
}

static void
test_file_directory_denied (TestCase *tc,
                            gconstpointer user_data)
{
  const gchar *roots[] = { srcdir, NULL };
  cockpit_web_response_file (tc->response, "/src", roots);
  cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 403 Directory Listing Denied*");
}

static void
test_file_access_denied (TestCase *tc,
                         gconstpointer user_data)
{
  const gchar *roots[] = { "/tmp", NULL };
  gchar templ[] = "/tmp/test-temp.XXXXXX";

  if (!g_mkdtemp_full (templ, 0000))
    g_assert_not_reached ();

  cockpit_web_response_file (tc->response, templ + 4, roots);
  cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 403*");

  g_unlink (templ);
}

static void
test_file_breakout_denied (TestCase *tc,
                           gconstpointer user_data)
{
  gchar *root = realpath ( SRCDIR "/src", NULL);
  const gchar *roots[] = { root, NULL };
  const gchar *breakout = "/../Makefile.am";
  gchar *check = g_build_filename (roots[0], breakout, NULL);
  g_assert (root);
  g_assert (g_file_test (check, G_FILE_TEST_EXISTS));
  g_free (check);
  cockpit_web_response_file (tc->response, breakout, roots);
  cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 404*");
  free (root);
}

static void
test_file_encoding_denied (TestCase *tc,
                           gconstpointer user_data)
{
  gchar *root = realpath ( SRCDIR "/src", NULL);
  const gchar *roots[] = { root, NULL };
  const gchar *breakout = "/common/Makefile-common.am%00";
  gchar *check = g_build_filename (roots[0], "common", "Makefile-common.am", NULL);
  g_assert (root);
  g_assert (g_file_test (check, G_FILE_TEST_EXISTS));
  g_free (check);
  cockpit_web_response_file (tc->response, breakout, roots);
  cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 404*");
  free (root);
}

static void
test_file_slash_denied (TestCase *tc,
                        gconstpointer user_data)
{
  gchar *root = realpath ( SRCDIR "/src", NULL);
  const gchar *roots[] = { root, NULL };
  const gchar *breakout = "/common%2fMakefile-common.am";
  gchar *check = g_build_filename (roots[0], "common", "Makefile-common.am", NULL);
  g_assert (root);
  g_assert (g_file_test (check, G_FILE_TEST_EXISTS));
  g_free (check);
  cockpit_web_response_file (tc->response, breakout, roots);
  cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 404*");
  free (root);
}

static void
test_file_breakout_non_existant (TestCase *tc,
                                 gconstpointer user_data)
{
  gchar *root = realpath ( SRCDIR "/src", NULL);
  const gchar *roots[] = { root, NULL };
  const gchar *breakout = "/../non-existent";
  gchar *check = g_build_filename (roots[0], breakout, NULL);
  g_assert (root);
  g_assert (!g_file_test (check, G_FILE_TEST_EXISTS));
  g_free (check);
  cockpit_web_response_file (tc->response, breakout, roots);
  cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 404*");
  free (root);
}

static const TestFixture content_type_fixture_html = {
  .path = "/pkg/shell/index.html",
  .expected_content_type = "text/html",
};

static const TestFixture content_type_fixture_png = {
  .path = "/pkg/shell/images/server-small.png",
  .expected_content_type = "image/png",
};

static const TestFixture content_type_fixture_wasm = {
  .path = "/src/common/mock-content/test.wasm",
  .expected_content_type = "application/wasm",
};

static const TestFixture fixture_head = {
  .method = "HEAD",
};

static const TestFixture fixture_unsupported_method = {
  .method = "PATCH",
};

static void
test_content_type (TestCase *tc,
                   gconstpointer user_data)
{
  const TestFixture *fixture = user_data;
  const gchar *roots[] = { srcdir, NULL };
  GHashTable *headers;
  const gchar *resp;
  gsize length;
  guint status;
  gssize off;

  cockpit_web_response_file (tc->response, NULL, roots);

  resp = output_as_string (tc);
  length = strlen (resp);

  off = web_socket_util_parse_status_line (resp, length, NULL, &status, NULL);
  g_assert_cmpuint (off, >, 0);
  g_assert_cmpint (status, ==, 200);

  off = web_socket_util_parse_headers (resp + off, length - off, &headers);
  g_assert_cmpuint (off, >, 0);

  g_assert_cmpstr (g_hash_table_lookup (headers, "Content-Type"), ==, fixture->expected_content_type);

  g_hash_table_unref (headers);
}

static const TestFixture template_fixture = {
  .path = "/test.css"
};

static void
test_template (TestCase *tc,
               gconstpointer user_data)
{
  const gchar *roots[] = { SRCDIR "/src/common/mock-content/", NULL };
  const gchar *resp;
  GHashTable *data = g_hash_table_new (g_str_hash, g_str_equal);

  g_hash_table_insert (data, "NAME", "test");
  g_hash_table_insert (data, "VARIANT", "VALUE");
  cockpit_web_response_template (tc->response, NULL, roots, data);

  resp = output_as_string (tc);
  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Type: text/css\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS "17\r\n#brand {\n    content: \"\r\n4\r\ntest\r\n4\r\n <b>\r\n5\r\nVALUE\r\n9\r\n</b>\";\n}\n\r\n0\r\n\r\n");
  g_hash_table_unref (data);
}

static const TestFixture cache_none_fixture = {
  .path = "/pkg/shell/index.html",
  .cache = COCKPIT_WEB_RESPONSE_NO_CACHE
};

static const TestFixture cache_fixture = {
  .path = "/pkg/shell/index.html",
  .cache = COCKPIT_WEB_RESPONSE_CACHE
};

static const TestFixture cache_unset_fixture = {
  .path = "/pkg/shell/index.html",
  .cache = COCKPIT_WEB_RESPONSE_CACHE_UNSET
};

static void
test_cache (TestCase *tc,
            gconstpointer user_data)
{
  const TestFixture *fixture = user_data;
  const gchar *roots[] = { srcdir, NULL };
  GHashTable *headers;
  const gchar *resp;
  gsize length;
  guint status;
  gssize off;

  cockpit_web_response_set_cache_type (tc->response, fixture->cache);
  cockpit_web_response_file (tc->response, NULL, roots);

  resp = output_as_string (tc);
  length = strlen (resp);

  off = web_socket_util_parse_status_line (resp, length, NULL, &status, NULL);
  g_assert_cmpuint (off, >, 0);
  g_assert_cmpint (status, ==, 200);

  off = web_socket_util_parse_headers (resp + off, length - off, &headers);
  g_assert_cmpuint (off, >, 0);

  if (fixture->cache == COCKPIT_WEB_RESPONSE_CACHE)
    g_assert_cmpstr (g_hash_table_lookup (headers, "Vary"), ==, "Cookie");
  else
    g_assert_null (g_hash_table_lookup (headers, "Vary"));

  if (fixture->cache == COCKPIT_WEB_RESPONSE_NO_CACHE)
    g_assert_cmpstr (g_hash_table_lookup (headers, "Cache-Control"), ==, "no-cache, no-store");
  else if (fixture->cache == COCKPIT_WEB_RESPONSE_CACHE)
    g_assert_cmpstr (g_hash_table_lookup (headers, "Cache-Control"), ==, "max-age=86400, private");
  else
    g_assert_null (g_hash_table_lookup (headers, "Cache-Control"));
  g_hash_table_unref (headers);
}

static void
test_content_encoding (TestCase *tc,
                       gconstpointer data)
{
  const gchar *resp;
  GBytes *content;

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);

  cockpit_web_response_headers (tc->response, 200, "OK", 50,
                                "Content-Encoding", "blah",
                                NULL);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);

  while (g_main_context_iteration (NULL, FALSE));

  content = g_bytes_new_static ("Cockpit is perfect for new sysadmins, ", 38);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  cockpit_web_response_complete (tc->response);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Encoding: blah\r\n"
                   "Transfer-Encoding: chunked\r\n" STATIC_HEADERS
                   "26\r\nCockpit is perfect for new sysadmins, \r\n0\r\n\r\n");
}

static void
test_stream (TestCase *tc,
             gconstpointer data)
{
  const gchar *resp;
  GBytes *content;

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);

  cockpit_web_response_headers (tc->response, 200, "OK", 11, NULL);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);

  while (g_main_context_iteration (NULL, FALSE));

  content = g_bytes_new_static ("the content", 11);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);

  cockpit_web_response_complete (tc->response);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n" STATIC_HEADERS "the content");
}

static void
on_pressure_set_throttle (CockpitWebResponse *response,
                          gboolean throttle,
                          gpointer user_data)
{
  gint *data = user_data;
  g_assert (user_data != NULL);
  *data = throttle ? 1 : 0;
}

static void
test_pressure (TestCase *tc,
               gconstpointer data)
{
  GBytes *sent;
  gint throttle = -1;
  gint i;

  g_signal_connect (tc->response, "pressure", G_CALLBACK (on_pressure_set_throttle), &throttle);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);

  cockpit_web_response_headers (tc->response, 200, "OK", -1, NULL);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);

  while (g_main_context_iteration (NULL, FALSE));

  /* Sent this a thousand times */
  sent = g_bytes_new_take (g_strnfill (10 * 1000, '?'), 10 * 1000);
  for (i = 0; i < 1000; i++)
    cockpit_web_response_queue (tc->response, sent);
  g_bytes_unref (sent);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);

  /*
   * This should have put way too much in the queue, and thus
   * emitted the back-pressure signal. This signal would normally
   * be used by others to slow down their queueing, but in this
   * case we just check that it was fired.
   */
  g_assert_cmpint (throttle, ==, 1);
  throttle = -1;

  cockpit_web_response_complete (tc->response);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);

  /*
   * Now the queue is getting drained. At some point, it will be
   * signaled that back pressure has been turned off
   */
  while (throttle == -1)
    g_main_context_iteration (NULL, TRUE);

  g_assert_cmpint (throttle, ==, 0);

  while (!tc->response_done)
    g_main_context_iteration (NULL, TRUE);
  g_assert_cmpint (g_memory_output_stream_get_data_size (G_MEMORY_OUTPUT_STREAM (tc->output)), >, 10 * 1000 * 1000);
}

static void
test_head (TestCase *tc,
           gconstpointer data)
{
  const gchar *resp;
  GBytes *content;

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);

  cockpit_web_response_headers (tc->response, 200, "OK", 19, NULL);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);

  while (g_main_context_iteration (NULL, FALSE));

  content = g_bytes_new_static ("I shall not be seen", 19);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);

  cockpit_web_response_complete (tc->response);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Length: 19\r\n" STATIC_HEADERS);
}

static void
test_unsupported_method (TestCase *tc,
                         gconstpointer data)
{
  cockpit_web_response_error (tc->response, 405, NULL, "Unsupported method");

  const gchar *resp = output_as_string (tc);
  g_assert (g_str_has_prefix (resp, "HTTP/1.1 405 Unsupported method\r\n"));
  /* not a HEAD request, thus has body */
  g_assert (strstr (resp, "<body>"));
}

static void
test_chunked_transfer_encoding (TestCase *tc,
                                gconstpointer data)
{
  const gchar *resp;
  GBytes *content;

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);

  cockpit_web_response_headers (tc->response, 200, "OK", -1, NULL);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);

  while (g_main_context_iteration (NULL, FALSE));

  content = g_bytes_new_static ("Cockpit is perfect for new sysadmins, ", 38);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  content = g_bytes_new_static ("allowing them to easily perform simple tasks such as storage administration, ", 77);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  content = g_bytes_new_static ("inspecting journals and starting and stopping services.", 55);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);

  cockpit_web_response_complete (tc->response);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
                   "26\r\nCockpit is perfect for new sysadmins, \r\n"
                   "4d\r\nallowing them to easily perform simple tasks such as storage administration, \r\n"
                   "37\r\ninspecting journals and starting and stopping services.\r\n0\r\n\r\n");
}

static void
test_chunked_zero_length (TestCase *tc,
                          gconstpointer data)
{
  const gchar *resp;
  GBytes *content;

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);

  cockpit_web_response_headers (tc->response, 200, "OK", -1, NULL);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);

  while (g_main_context_iteration (NULL, FALSE));

  content = g_bytes_new_static ("Cockpit is perfect for new sysadmins, ", 38);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  content = g_bytes_new_static ("", 0);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  content = g_bytes_new_static ("inspecting journals and starting and stopping services.", 55);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  content = g_bytes_new_static ("", 0);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
  cockpit_web_response_complete (tc->response);

  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
                   "26\r\nCockpit is perfect for new sysadmins, \r\n"
                   "37\r\ninspecting journals and starting and stopping services.\r\n0\r\n\r\n");
}

static GBytes *
bytes_static (const gchar *data)
{
  return g_bytes_new_static (data, strlen (data));
}

static void
test_web_filter_simple (TestCase *tc,
                        gconstpointer data)
{
  CockpitWebFilter *filter;
  const gchar *resp;
  GBytes *content;
  GBytes *inject;

  inject = bytes_static ("<meta inject>");
  filter = cockpit_web_inject_new ("<head>", inject, 1);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  content = bytes_static ("<html><head><title>The Title</title></head></html>");
  cockpit_web_response_content (tc->response, NULL, content, NULL);
  g_bytes_unref (content);

  while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
    g_main_context_iteration (NULL, TRUE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
                   "c\r\n<html><head>\r\n"
                   "d\r\n<meta inject>\r\n"
                   "26\r\n<title>The Title</title></head></html>\r\n"
                   "0\r\n\r\n");
}

static void
test_web_filter_multiple (TestCase *tc,
                          gconstpointer data)
{
  CockpitWebFilter *filter;
  const gchar *resp;
  GBytes *content;
  GBytes *inject;

  inject = bytes_static ("<meta inject>");
  filter = cockpit_web_inject_new ("<head>", inject, 1);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  inject = bytes_static ("<body>Body</body>");
  filter = cockpit_web_inject_new ("</head>", inject, 1);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  inject = bytes_static ("Prefix ");
  filter = cockpit_web_inject_new ("<title>", inject, 1);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  inject = bytes_static (" ");
  filter = cockpit_web_inject_new (">", inject, 3);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  content = bytes_static ("<html><head><title>The Title</title></head></html>");
  cockpit_web_response_content (tc->response, NULL, content, NULL);
  g_bytes_unref (content);

  while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
    g_main_context_iteration (NULL, TRUE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
                   "6\r\n<html>\r\n"
                   "1\r\n \r\n"
                   "6\r\n<head>\r\n"
                   "1\r\n \r\n"
                   "d\r\n<meta inject>\r\n"
                   "1\r\n \r\n"
                   "7\r\n<title>\r\n"
                   "7\r\nPrefix \r\n"
                   "18\r\nThe Title</title></head>\r\n"
                   "11\r\n<body>Body</body>\r\n"
                   "7\r\n</html>\r\n"
                   "0\r\n\r\n");
}

static void
test_web_filter_split (TestCase *tc,
                       gconstpointer data)
{
  CockpitWebFilter *filter;
  const gchar *string;
  const gchar *resp;
  GBytes *inject;
  GBytes *block;
  gsize i, x, len;

  inject = bytes_static ("<meta inject>");
  filter = cockpit_web_inject_new ("<head>", inject, 1);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  inject = bytes_static ("<body>Body</body>");
  filter = cockpit_web_inject_new ("</head>", inject, 1);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  inject = bytes_static ("Prefix ");
  filter = cockpit_web_inject_new ("<title>", inject, 1);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  cockpit_web_response_headers (tc->response, 200, "OK", -1, NULL);

  string = "<html><head><title>The Title</title></head></html>";
  len = strlen (string);

  for (i = 0, x = 1; i < len; i += x, x = 1 + (i % 4))
    {
      block = g_bytes_new_static (string + i, MIN (x, strlen (string + i)));
      g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
      g_bytes_unref (block);
    }

  cockpit_web_response_complete (tc->response);

  while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
    g_main_context_iteration (NULL, TRUE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
                   "1\r\n<\r\n"
                   "2\r\nht\r\n"
                   "4\r\nml><\r\n"
                   "4\r\nhead\r\n"
                   "1\r\n>\r\n"
                   "d\r\n<meta inject>\r\n"
                   "3\r\n<ti\r\n"
                   "4\r\ntle>\r\n"
                   "7\r\nPrefix \r\n"
                   "4\r\nThe \r\n"
                   "4\r\nTitl\r\n"
                   "4\r\ne</t\r\n"
                   "4\r\nitle\r\n"
                   "4\r\n></h\r\n"
                   "4\r\nead>\r\n"
                   "11\r\n<body>Body</body>\r\n"
                   "4\r\n</ht\r\n"
                   "3\r\nml>\r\n"
                   "0\r\n\r\n");
}

static void
test_web_filter_shift (TestCase *tc,
                       gconstpointer data)
{
  CockpitWebFilter *filter;
  const gchar *resp;
  GBytes *block;
  GBytes *inject;

  inject = bytes_static ("injected");
  filter = cockpit_web_inject_new ("foofn", inject, 1);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  cockpit_web_response_headers_full (tc->response, 200, "OK", -1, NULL);

  /* Total content is foofoofn and split after the first 4 characters */
  block = bytes_static ("foof");
  g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
  g_bytes_unref (block);
  block = bytes_static ("oofn");
  g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
  g_bytes_unref (block);
  cockpit_web_response_complete (tc->response);

  while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
    g_main_context_iteration (NULL, TRUE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
                   "4\r\nfoof\r\n"
                   "4\r\noofn\r\n"
                   "8\r\ninjected\r\n"
                   "0\r\n\r\n");
}

static void
test_web_filter_shift_three (TestCase *tc,
                             gconstpointer data)
{
  CockpitWebFilter *filter;
  const gchar *resp;
  GBytes *block;
  GBytes *inject;

  inject = bytes_static ("injected");
  filter = cockpit_web_inject_new ("foofn", inject, 1);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  cockpit_web_response_headers_full (tc->response, 200, "OK", -1, NULL);

  /* Total content is foofoofn and split across multiple packets after the first 4 characters */
  block = bytes_static ("foof");
  g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
  g_bytes_unref (block);
  block = bytes_static ("o");
  g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
  g_bytes_unref (block);
  block = bytes_static ("of");
  g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
  g_bytes_unref (block);
  block = bytes_static ("n");
  g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
  g_bytes_unref (block);
  cockpit_web_response_complete (tc->response);

  while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
    g_main_context_iteration (NULL, TRUE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
                   "4\r\nfoof\r\n"
                   "1\r\no\r\n"
                   "2\r\nof\r\n"
                   "1\r\nn\r\n"
                   "8\r\ninjected\r\n"
                   "0\r\n\r\n");
}

static void
test_web_filter_passthrough (TestCase *tc,
                             gconstpointer data)
{
  CockpitWebFilter *filter;
  const gchar *resp;
  GBytes *content;
  GBytes *inject;

  inject = bytes_static ("<meta inject>");
  filter = cockpit_web_inject_new ("<unknown>", inject, 1);
  cockpit_web_response_add_filter (tc->response, filter);
  g_object_unref (filter);
  g_bytes_unref (inject);

  content = bytes_static ("<html><head><title>The Title</title></head></html>");
  cockpit_web_response_content (tc->response, NULL, content, NULL);
  g_bytes_unref (content);

  while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
    g_main_context_iteration (NULL, TRUE);

  resp = output_as_string (tc);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
                   "32\r\n<html><head><title>The Title</title></head></html>\r\n"
                   "0\r\n\r\n");
}

static void
on_response_done_not_resuable (CockpitWebResponse *response,
                               gboolean reusable,
                               gpointer user_data)
{
  g_assert (reusable == FALSE);
}

static void
test_abort (TestCase *tc,
            gconstpointer data)
{
  const gchar *resp;
  GBytes *content;

  cockpit_web_response_headers (tc->response, 200, "OK", 11, NULL);
  g_signal_connect (tc->response, "done", G_CALLBACK (on_response_done_not_resuable), NULL);

  while (g_main_context_iteration (NULL, FALSE));

  content = g_bytes_new_static ("the content", 11);
  cockpit_web_response_queue (tc->response, content);
  g_bytes_unref (content);

  cockpit_web_response_abort (tc->response);
  g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);

  resp = output_as_string (tc);

  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n" STATIC_HEADERS);
}

static const TestFixture fixture_connection_close = {
  .header = "Connection",
  .value = "close",
};

static void
test_connection_close (TestCase *tc,
                       gconstpointer data)
{
  const gchar *resp;
  GBytes *content;

  g_assert (data == &fixture_connection_close);

  g_signal_connect (tc->response, "done", G_CALLBACK (on_response_done_not_resuable), NULL);

  content = g_bytes_new_static ("the content", 11);
  cockpit_web_response_content (tc->response, NULL, content, NULL);
  g_bytes_unref (content);

  resp = output_as_string (tc);
  g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Length: 11\r\nConnection: close\r\n"
                   STATIC_HEADERS "the content");
}

static const TestFixture fixture_origin_default = {
  .header = "Host",
  .value = "somemachine",
  .for_tls_proxy = FALSE,
};

static const TestFixture fixture_origin_tls_proxy = {
  .header = "Host",
  .value = "somemachine",
  .for_tls_proxy = TRUE,
};

static void
test_origin (TestCase *tc,
             gconstpointer data)
{
  const TestFixture *fixture = data;
  GBytes *content;

  if (fixture->for_tls_proxy)
    g_assert_cmpstr (cockpit_web_response_get_origin (tc->response), ==, "https://somemachine");
  else
    g_assert_cmpstr (cockpit_web_response_get_origin (tc->response), ==, "http://somemachine");

  content = g_bytes_new_static ("the content", 11);
  cockpit_web_response_content (tc->response, NULL, content, NULL);
  g_bytes_unref (content);
  output_as_string (tc);
}

typedef struct {
    GHashTable *headers;
    GIOStream *io;
} TestPlain;

static void
setup_plain (TestPlain *tc,
             gconstpointer unused)
{
  GInputStream *input;
  GOutputStream *output;

  input = g_memory_input_stream_new ();
  output = g_memory_output_stream_new (NULL, 0, g_realloc, g_free);

  tc->io = g_simple_io_stream_new (input, output);

  g_object_unref (input);
  g_object_unref (output);

  tc->headers = cockpit_web_server_new_table ();
}

static void
teardown_plain (TestPlain *tc,
                gconstpointer unused)
{
  g_object_unref (tc->io);
  g_hash_table_unref (tc->headers);
}

static void
test_pop_path (TestPlain *tc,
               gconstpointer unused)
{
  CockpitWebResponse *response;
  gchar *part;
  const gchar *start = "/cockpit/@localhost/another/test.html";

  response = cockpit_web_response_new (tc->io, start, start, tc->headers, "GET", NULL);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, start);
  g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, NULL);

  part = cockpit_web_response_pop_path (response);
  g_assert_cmpstr (part, ==, "cockpit");
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/@localhost/another/test.html");
  g_free (part);

  part = cockpit_web_response_pop_path (response);
  g_assert_cmpstr (part, ==, "@localhost");
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/another/test.html");
  g_free (part);

  part = cockpit_web_response_pop_path (response);
  g_assert_cmpstr (part, ==, "another");
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/test.html");
  g_free (part);

  part = cockpit_web_response_pop_path (response);
  g_assert_cmpstr (part, ==, "test.html");
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, NULL);
  g_free (part);

  part = cockpit_web_response_pop_path (response);
  g_assert (part == NULL);
  g_assert (cockpit_web_response_get_path (response) == NULL);
  g_free (part);

  cockpit_web_response_abort (response);
  g_object_unref (response);
}

static void
test_pop_path_root (TestPlain *tc,
                    gconstpointer unused)
{
  CockpitWebResponse *response;
  gchar *part;

  response = cockpit_web_response_new (tc->io, "/", "/", tc->headers, "GET", NULL);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/");

  part = cockpit_web_response_pop_path (response);
  g_assert_cmpstr (part, ==, NULL);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, NULL);
  g_free (part);

  cockpit_web_response_abort (response);
  g_object_unref (response);
}

static void
test_skip_path (TestPlain *tc,
                gconstpointer unused)
{
  CockpitWebResponse *response;
  const gchar *start = "/cockpit/@localhost/another/test.html";

  response = cockpit_web_response_new (tc->io, start, start, tc->headers, "GET", NULL);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/cockpit/@localhost/another/test.html");

  g_assert (cockpit_web_response_skip_path (response) == TRUE);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/@localhost/another/test.html");

  g_assert (cockpit_web_response_skip_path (response) == TRUE);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/another/test.html");

  g_assert (cockpit_web_response_skip_path (response) == TRUE);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/test.html");

  g_assert (cockpit_web_response_skip_path (response) == TRUE);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, NULL);

  g_assert (cockpit_web_response_skip_path (response) == FALSE);
  g_assert (cockpit_web_response_get_path (response) == NULL);

  cockpit_web_response_abort (response);
  g_object_unref (response);
}

static void
test_skip_path_root (TestPlain *tc,
                     gconstpointer unused)
{
  CockpitWebResponse *response;

  response = cockpit_web_response_new (tc->io, "/", "/", tc->headers, "GET", NULL);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/");

  g_assert (cockpit_web_response_skip_path (response) == FALSE);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, NULL);

  cockpit_web_response_abort (response);
  g_object_unref (response);
}

static void
test_removed_prefix (TestPlain *tc,
                     gconstpointer unused)
{
  CockpitWebResponse *response;

  response = cockpit_web_response_new (tc->io, "/", "/", tc->headers, "GET", NULL);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/");
  g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, NULL);
  cockpit_web_response_abort (response);
  g_clear_object (&response);

  response = cockpit_web_response_new (tc->io, "/path/", "/path/", tc->headers, "GET", NULL);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/path/");
  g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, NULL);
  cockpit_web_response_abort (response);
  g_clear_object (&response);

  response = cockpit_web_response_new (tc->io, "/path/path2/", "/path2/", tc->headers, "GET", NULL);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/path2/");
  g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, "/path");
  cockpit_web_response_abort (response);
  g_clear_object (&response);

  response = cockpit_web_response_new (tc->io, "/mis/", "/match/", tc->headers, "GET", NULL);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/match/");
  g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, NULL);
  cockpit_web_response_abort (response);
  g_clear_object (&response);

  response = cockpit_web_response_new (tc->io, NULL, NULL, tc->headers, "GET", NULL);
  g_assert_cmpstr (cockpit_web_response_get_path (response), ==, NULL);
  g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, NULL);
  cockpit_web_response_abort (response);
  g_clear_object (&response);
}

static void
test_gunzip_small (void)
{
  GError *error = NULL;
  GMappedFile *file;
  GBytes *compressed;
  GBytes *bytes;

  file = g_mapped_file_new (SRCDIR "/src/common/mock-content/test-file.txt.gz", FALSE, &error);
  g_assert_no_error (error);

  compressed = g_mapped_file_get_bytes (file);
  g_mapped_file_unref (file);

  bytes = cockpit_web_response_gunzip (compressed, &error);
  g_assert_no_error (error);
  g_bytes_unref (compressed);

  cockpit_assert_bytes_eq (bytes, "A small test file\n", -1);
  g_bytes_unref (bytes);
}

static void
test_gunzip_large (void)
{
  GError *error = NULL;
  GMappedFile *file;
  GBytes *compressed;
  GBytes *bytes;
  gchar *checksum;

  file = g_mapped_file_new (SRCDIR "/src/common/mock-content/large.min.js.gz", FALSE, &error);
  g_assert_no_error (error);

  compressed = g_mapped_file_get_bytes (file);
  g_mapped_file_unref (file);

  bytes = cockpit_web_response_gunzip (compressed, &error);
  g_assert_no_error (error);
  g_bytes_unref (compressed);

  checksum = g_compute_checksum_for_bytes (G_CHECKSUM_MD5, bytes);
  g_assert_cmpstr (checksum, ==, "5ca7582261c421482436dfdf3af9bffe");
  g_free (checksum);

  g_bytes_unref (bytes);
}

static void
test_gunzip_invalid (void)
{
  GError *error = NULL;
  GBytes *compressed;
  GBytes *bytes;

  compressed = g_bytes_new_static ("invalid", 7);

  bytes = cockpit_web_response_gunzip (compressed, &error);
  g_assert (bytes == NULL);
  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA);
  g_error_free (error);

  g_bytes_unref (compressed);
}

static void
test_negotiation_first (void)
{
  gboolean is_language_specific, is_compressed;
  GError *error = NULL;
  GBytes *bytes;

  bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/test-file.txt",
                                            NULL, NULL, &is_language_specific, &is_compressed, &error);

  cockpit_assert_bytes_eq (bytes, "A small test file\n", -1);
  g_assert_no_error (error);
  g_bytes_unref (bytes);

  g_assert (!is_language_specific);
  g_assert (!is_compressed);
}

static void
test_negotiation_last (void)
{
  gboolean is_language_specific, is_compressed;
  GError *error = NULL;
  gchar *checksum;
  GBytes *bytes;

  bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/large.js",
                                            NULL, NULL, &is_language_specific, &is_compressed, &error);

  g_assert_no_error (error);
  g_assert (!is_language_specific);
  g_assert (is_compressed);

  checksum = g_compute_checksum_for_bytes (G_CHECKSUM_MD5, bytes);
  g_assert_cmpstr (checksum, ==, "e5284b625b7665fc04e082827de3436c");
  g_free (checksum);

  g_bytes_unref (bytes);
}

static void
test_negotiation_prune (void)
{
  GError *error = NULL;
  GBytes *bytes;

  bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/test-file.extra.extension.txt",
                                            NULL, NULL, NULL, NULL, &error);

  cockpit_assert_bytes_eq (bytes, "A small test file\n", -1);
  g_assert_no_error (error);
  g_bytes_unref (bytes);
}

static void
test_negotiation_with_listing (void)
{
  GHashTable *existing;
  GError *error = NULL;
  GBytes *bytes;

  /* Lie and say that only the .gz file exists */
  existing = g_hash_table_new (g_str_hash, g_str_equal);
  g_hash_table_add (existing, SRCDIR "/src/common/mock-content/test-file.txt.gz");

  bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/test-file.txt",
                                            existing, NULL, NULL, NULL, &error);

  cockpit_assert_bytes_eq (bytes, "\x1F\x8B\x08\x08N1\x03U\x00\x03test-file.txt\x00"
                           "sT(\xCEM\xCC\xC9Q(I-.QH\xCB\xCCI\xE5\x02\x00>PjG\x12\x00\x00\x00", 52);
  g_assert_no_error (error);
  g_bytes_unref (bytes);

  g_hash_table_unref (existing);
}

static void
test_negotiation_locale (void)
{
  gboolean is_language_specific, is_compressed;
  GError *error = NULL;
  GBytes *bytes;

  bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/test-file.txt",
                                            NULL, "zh-cn", &is_language_specific, &is_compressed, &error);

  cockpit_assert_bytes_eq (bytes, "A translated test file\n", -1);
  g_assert_no_error (error);
  g_bytes_unref (bytes);

  g_assert (is_language_specific);
  g_assert (!is_compressed);
}

static void
test_negotiation_notfound (void)
{
  GError *error = NULL;
  GBytes *bytes;

  bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/non-existent",
                                            NULL, NULL, NULL, NULL, &error);

  g_assert_no_error (error);
  g_assert (bytes == NULL);
}

static void
test_negotiation_failure (void)
{
  GError *error = NULL;
  GBytes *bytes;

  bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/directory",
                                            NULL, NULL, NULL, NULL, &error);

  g_assert (error != NULL);
  g_error_free (error);

  g_assert (bytes == NULL);
}

int
main (int argc,
      char *argv[])
{
  extern const gchar *cockpit_webresponse_fail_html_text;
  cockpit_webresponse_fail_html_text =
    "<html><head><title>@@message@@</title></head><body>@@message@@</body></html>\n";

  gint ret;

  srcdir = realpath (SRCDIR, NULL);
  g_assert (srcdir != NULL);

  cockpit_test_init (&argc, &argv);

  g_test_add ("/web-response/return-content", TestCase, NULL,
              setup, test_return_content, teardown);
  g_test_add ("/web-response/return-content-headers", TestCase, NULL,
              setup, test_return_content_headers, teardown);
  g_test_add ("/web-response/return-error", TestCase, NULL,
              setup, test_return_error, teardown);
  g_test_add ("/web-response/return-error-headers", TestCase, NULL,
              setup, test_return_error_headers, teardown);
  g_test_add ("/web-response/return-gerror-headers", TestCase, NULL,
              setup, test_return_gerror_headers, teardown);
  g_test_add ("/web-response/file/not-found", TestCase, NULL,
              setup, test_file_not_found, teardown);
  g_test_add ("/web-response/file/directory-denied", TestCase, NULL,
              setup, test_file_directory_denied, teardown);
  g_test_add ("/web-response/file/access-denied", TestCase, NULL,
              setup, test_file_access_denied, teardown);
  g_test_add ("/web-response/file/breakout-denied", TestCase, NULL,
              setup, test_file_breakout_denied, teardown);
  g_test_add ("/web-response/file/invalid-encoding-denied", TestCase, NULL,
              setup, test_file_encoding_denied, teardown);
  g_test_add ("/web-response/file/file-slash-denied", TestCase, NULL,
              setup, test_file_slash_denied, teardown);
  g_test_add ("/web-response/file/breakout-non-existant", TestCase, NULL,
              setup, test_file_breakout_non_existant, teardown);
  g_test_add ("/web-reponse/file/template", TestCase, &template_fixture,
              setup, test_template, teardown);
  g_test_add ("/web-response/content-type/html", TestCase, &content_type_fixture_html,
              setup, test_content_type, teardown);
  g_test_add ("/web-response/content-type/png", TestCase, &content_type_fixture_png,
              setup, test_content_type, teardown);
  g_test_add ("/web-response/content-type/wasm", TestCase, &content_type_fixture_wasm,
              setup, test_content_type, teardown);
  g_test_add ("/web-response/content-encoding", TestCase, NULL,
              setup, test_content_encoding, teardown);
  g_test_add ("/web-response/stream", TestCase, NULL,
              setup, test_stream, teardown);
  g_test_add ("/web-response/pressure", TestCase, NULL,
              setup, test_pressure, teardown);
  g_test_add ("/web-response/head", TestCase, &fixture_head,
              setup, test_head, teardown);
  g_test_add ("/web-response/unsupported-method", TestCase, &fixture_unsupported_method,
              setup, test_unsupported_method, teardown);
  g_test_add ("/web-response/chunked-transfer-encoding", TestCase, NULL,
              setup, test_chunked_transfer_encoding, teardown);
  g_test_add ("/web-response/chunked-zero-length", TestCase, NULL,
              setup, test_chunked_zero_length, teardown);
  g_test_add ("/web-response/abort", TestCase, NULL,
              setup, test_abort, teardown);
  g_test_add ("/web-response/connection-close", TestCase, &fixture_connection_close,
              setup, test_connection_close, teardown);
  g_test_add ("/web-response/origin-default", TestCase, &fixture_origin_default,
              setup, test_origin, teardown);
  g_test_add ("/web-response/origin-tls-proxy", TestCase, &fixture_origin_tls_proxy,
              setup, test_origin, teardown);

  g_test_add ("/web-response/cache", TestCase, &cache_fixture,
              setup, test_cache, teardown);
  g_test_add ("/web-response/cache-none", TestCase, &cache_none_fixture,
              setup, test_cache, teardown);
  g_test_add ("/web-response/cache-unset", TestCase, &cache_unset_fixture,
              setup, test_cache, teardown);

  g_test_add ("/web-response/filter/simple", TestCase, NULL,
              setup, test_web_filter_simple, teardown);
  g_test_add ("/web-response/filter/multiple", TestCase, NULL,
              setup, test_web_filter_multiple, teardown);
  g_test_add ("/web-response/filter/passthrough", TestCase, NULL,
              setup, test_web_filter_passthrough, teardown);
  g_test_add ("/web-response/filter/split", TestCase, NULL,
              setup, test_web_filter_split, teardown);
  g_test_add ("/web-response/filter/shift", TestCase, NULL,
              setup, test_web_filter_shift, teardown);
  g_test_add ("/web-response/filter/shift_three", TestCase, NULL,
              setup, test_web_filter_shift_three, teardown);

  g_test_add ("/web-response/path/pop", TestPlain, NULL,
              setup_plain, test_pop_path, teardown_plain);
  g_test_add ("/web-response/path/pop-root", TestPlain, NULL,
              setup_plain, test_pop_path_root, teardown_plain);
  g_test_add ("/web-response/path/skip", TestPlain, NULL,
              setup_plain, test_skip_path, teardown_plain);
  g_test_add ("/web-response/path/skip-root", TestPlain, NULL,
              setup_plain, test_skip_path_root, teardown_plain);
  g_test_add ("/web-response/path/removed-prefix", TestPlain, NULL,
              setup_plain, test_removed_prefix, teardown_plain);

  g_test_add_func ("/web-response/gunzip/small", test_gunzip_small);
  g_test_add_func ("/web-response/gunzip/large", test_gunzip_large);
  g_test_add_func ("/web-response/gunzip/invalid", test_gunzip_invalid);

  g_test_add_func ("/web-response/negotiation/first", test_negotiation_first);
  g_test_add_func ("/web-response/negotiation/last", test_negotiation_last);
  g_test_add_func ("/web-response/negotiation/locale", test_negotiation_locale);
  g_test_add_func ("/web-response/negotiation/prune", test_negotiation_prune);
  g_test_add_func ("/web-response/negotiation/with-listing", test_negotiation_with_listing);
  g_test_add_func ("/web-response/negotiation/notfound", test_negotiation_notfound);
  g_test_add_func ("/web-response/negotiation/failure", test_negotiation_failure);

  ret = g_test_run ();

  free (srcdir);

  return ret;
}
