/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/*
* GData Client
* Copyright (C) Philip Withnall 2010, 2015 <philip@tecnocode.co.uk>
*
* GData Client 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.
*
* GData Client 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 GData Client. If not, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <glib.h>
#include <locale.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include "gdata.h"
#include "common.h"
#ifdef HAVE_LIBSOUP_2_55_90
static gpointer
run_server_thread (GMainLoop *loop)
{
g_main_context_push_thread_default (g_main_loop_get_context (loop));
g_main_loop_run (loop);
g_main_context_pop_thread_default (g_main_loop_get_context (loop));
return NULL;
}
#else /* if !HAVE_LIBSOUP_2_55_90 */
static gpointer
run_server_thread (SoupServer *server)
{
soup_server_run (server);
return NULL;
}
#endif /* !HAVE_LIBSOUP_2_55_90 */
static GThread *
run_server (SoupServer *server, GMainLoop *loop)
{
GThread *thread;
gchar *port_string;
GError *error = NULL;
guint16 port;
#ifdef HAVE_LIBSOUP_2_55_90
thread = g_thread_try_new ("server-thread", (GThreadFunc) run_server_thread, loop, &error);
#else /* if !HAVE_LIBSOUP_2_55_90 */
thread = g_thread_try_new ("server-thread", (GThreadFunc) run_server_thread, server, &error);
#endif /* !HAVE_LIBSOUP_2_55_90 */
g_assert_no_error (error);
g_assert (thread != NULL);
/* Set the port so that libgdata doesn't override it. */
#ifdef HAVE_LIBSOUP_2_55_90
{
GSList *uris; /* owned */
uris = soup_server_get_uris (server);
g_assert (uris != NULL);
port = soup_uri_get_port (uris->data);
g_slist_free_full (uris, (GDestroyNotify) soup_uri_free);
}
#else /* if !HAVE_LIBSOUP_2_55_90 */
port = soup_server_get_port (server);
#endif /* !HAVE_LIBSOUP_2_55_90 */
port_string = g_strdup_printf ("%u", port);
g_setenv ("LIBGDATA_HTTPS_PORT", port_string, TRUE);
g_free (port_string);
return thread;
}
#ifdef HAVE_LIBSOUP_2_55_90
static gboolean
quit_server_cb (GMainLoop *loop)
{
g_main_loop_quit (loop);
return FALSE;
}
static void
stop_server (SoupServer *server, GMainLoop *loop)
{
soup_add_completion (g_main_loop_get_context (loop),
(GSourceFunc) quit_server_cb, loop);
}
#else /* if !HAVE_LIBSOUP_2_55_90 */
static gboolean
quit_server_cb (SoupServer *server)
{
soup_server_quit (server);
return FALSE;
}
static void
stop_server (SoupServer *server, GMainLoop *loop)
{
soup_add_completion (g_main_loop_get_context (loop),
(GSourceFunc) quit_server_cb, server);
}
#endif /* !HAVE_LIBSOUP_2_55_90 */
static gchar *
get_test_string (guint start_num, guint end_num)
{
GString *test_string;
guint i;
g_return_val_if_fail (end_num < G_MAXUINT, NULL);
test_string = g_string_new (NULL);
for (i = start_num; i < end_num + 1; i++)
g_string_append_printf (test_string, "%u\n", i);
return g_string_free (test_string, FALSE);
}
static void
test_download_stream_download_server_content_length_handler_cb (SoupServer *server, SoupMessage *message, const char *path, GHashTable *query,
SoupClientContext *client, gpointer user_data)
{
gchar *test_string;
goffset test_string_length;
test_string = get_test_string (1, 1000);
test_string_length = strlen (test_string) + 1;
/* Add some response headers */
soup_message_set_status (message, SOUP_STATUS_OK);
soup_message_headers_set_content_type (message->response_headers, "text/plain", NULL);
soup_message_headers_set_content_length (message->response_headers, test_string_length);
/* Set the response body */
soup_message_body_append (message->response_body, SOUP_MEMORY_TAKE, test_string, test_string_length);
}
static SoupServer *
create_server (SoupServerCallback callback, gpointer user_data, GMainLoop **main_loop)
{
GMainContext *context;
SoupServer *server;
#ifdef HAVE_LIBSOUP_2_55_90
gchar *cert_path = NULL, *key_path = NULL;
GError *error = NULL;
#else /* if !HAVE_LIBSOUP_2_55_90 */
union {
struct sockaddr_in in;
struct sockaddr norm;
} sock;
SoupAddress *addr;
#endif /* HAVE_LIBSOUP_2_55_90 */
/* Create the server */
g_assert (main_loop != NULL);
context = g_main_context_new ();
*main_loop = g_main_loop_new (context, FALSE);
#ifdef HAVE_LIBSOUP_2_55_90
server = soup_server_new (NULL, NULL);
cert_path = g_test_build_filename (G_TEST_DIST, "cert.pem", NULL);
key_path = g_test_build_filename (G_TEST_DIST, "key.pem", NULL);
soup_server_set_ssl_cert_file (server, cert_path, key_path, &error);
g_assert_no_error (error);
g_free (key_path);
g_free (cert_path);
soup_server_add_handler (server, NULL, callback, user_data, NULL);
g_main_context_push_thread_default (context);
soup_server_listen_local (server, 0 /* random port */,
SOUP_SERVER_LISTEN_HTTPS, &error);
g_assert_no_error (error);
g_main_context_pop_thread_default (context);
#else /* if !HAVE_LIBSOUP_2_55_90 */
memset (&sock, 0, sizeof (sock));
sock.in.sin_family = AF_INET;
sock.in.sin_addr.s_addr = htonl (INADDR_LOOPBACK);
sock.in.sin_port = htons (0); /* random port */
addr = soup_address_new_from_sockaddr (&sock.norm, sizeof (sock.norm));
g_assert (addr != NULL);
server = soup_server_new (SOUP_SERVER_INTERFACE, addr,
SOUP_SERVER_ASYNC_CONTEXT, context,
NULL);
soup_server_add_handler (server, NULL, callback, user_data, NULL);
g_object_unref (addr);
#endif /* !HAVE_LIBSOUP_2_55_90 */
g_assert (server != NULL);
g_main_context_unref (context);
return server;
}
static gchar *
build_server_uri (SoupServer *server)
{
#ifdef HAVE_LIBSOUP_2_55_90
GSList *uris; /* owned */
GSList *l; /* unowned */
gchar *retval = NULL; /* owned */
uris = soup_server_get_uris (server);
for (l = uris; l != NULL && retval == NULL; l = l->next) {
if (soup_uri_get_scheme (l->data) == SOUP_URI_SCHEME_HTTPS) {
retval = soup_uri_to_string (l->data, FALSE);
}
}
g_slist_free_full (uris, (GDestroyNotify) soup_uri_free);
g_assert (retval != NULL);
return retval;
#else /* if !HAVE_LIBSOUP_2_55_90 */
return g_strdup_printf ("https://%s:%u/",
soup_address_get_physical (soup_socket_get_local_address (soup_server_get_listener (server))),
soup_server_get_port (server));
#endif /* !HAVE_LIBSOUP_2_55_90 */
}
static void
test_download_stream_download_content_length (void)
{
SoupServer *server;
GMainLoop *main_loop;
GThread *thread;
gchar *download_uri, *test_string;
GDataService *service;
GInputStream *download_stream;
gssize length_read;
GString *contents;
guint8 buffer[20];
gboolean success;
GError *error = NULL;
/* Create and run the server */
server = create_server ((SoupServerCallback) test_download_stream_download_server_content_length_handler_cb, NULL, &main_loop);
thread = run_server (server, main_loop);
/* Create a new download stream connected to the server */
download_uri = build_server_uri (server);
service = GDATA_SERVICE (gdata_youtube_service_new ("developer-key", NULL));
download_stream = gdata_download_stream_new (service, NULL, download_uri, NULL);
g_object_unref (service);
g_free (download_uri);
/* Read the entire stream into a string which we can later compare with what we expect. */
contents = g_string_new (NULL);
while ((length_read = g_input_stream_read (download_stream, buffer, sizeof (buffer), NULL, &error)) > 0) {
g_assert_cmpint (length_read, <=, sizeof (buffer));
g_string_append_len (contents, (const gchar*) buffer, length_read);
}
/* Check we've reached EOF successfully */
g_assert_no_error (error);
g_assert_cmpint (length_read, ==, 0);
/* Close the stream */
success = g_input_stream_close (download_stream, NULL, &error);
g_assert_no_error (error);
g_assert (success == TRUE);
/* Compare the downloaded string to the original */
test_string = get_test_string (1, 1000);
g_assert_cmpint (contents->len, ==, strlen (test_string) + 1);
g_assert_cmpstr (contents->str, ==, test_string);
g_free (test_string);
g_string_free (contents, TRUE);
/* Kill the server and wait for it to die */
stop_server (server, main_loop);
g_thread_join (thread);
g_object_unref (download_stream);
g_object_unref (server);
g_main_loop_unref (main_loop);
}
static void
test_download_stream_download_server_seek_handler_cb (SoupServer *server, SoupMessage *message, const char *path, GHashTable *query,
SoupClientContext *client, gpointer user_data)
{
gchar *test_string;
goffset test_string_length;
test_string = get_test_string (1, 1000);
test_string_length = strlen (test_string) + 1;
/* Add some response headers */
soup_message_set_status (message, SOUP_STATUS_OK);
soup_message_body_append (message->response_body, SOUP_MEMORY_TAKE, test_string, test_string_length);
}
/* Test seeking before the first read */
static void
test_download_stream_download_seek_before_start (void)
{
SoupServer *server;
GMainLoop *main_loop;
GThread *thread;
gchar *download_uri, *test_string;
goffset test_string_offset = 0;
guint test_string_length;
GDataService *service;
GInputStream *download_stream;
gssize length_read;
guint8 buffer[20];
gboolean success;
GError *error = NULL;
/* Create and run the server */
server = create_server ((SoupServerCallback) test_download_stream_download_server_seek_handler_cb, NULL, &main_loop);
thread = run_server (server, main_loop);
/* Create a new download stream connected to the server */
download_uri = build_server_uri (server);
service = GDATA_SERVICE (gdata_youtube_service_new ("developer-key", NULL));
download_stream = gdata_download_stream_new (service, NULL, download_uri, NULL);
g_object_unref (service);
g_free (download_uri);
/* Read alternating blocks into a string and compare with what we expect as we go. i.e. Skip 20 bytes, then read 20 bytes, etc. */
test_string = get_test_string (1, 1000);
test_string_length = strlen (test_string) + 1;
while (TRUE) {
/* Check the seek offset */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
/* Seek forward a buffer length */
if (g_seekable_seek (G_SEEKABLE (download_stream), sizeof (buffer), G_SEEK_CUR, NULL, &error) == FALSE) {
/* Tried to seek past the end of the stream */
g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT);
g_clear_error (&error);
break;
}
test_string_offset += sizeof (buffer);
g_assert_no_error (error);
/* Check the seek offset again */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
/* Read a buffer-load */
length_read = g_input_stream_read (download_stream, buffer, sizeof (buffer), NULL, &error);
g_assert_no_error (error);
g_assert_cmpint (length_read, <=, sizeof (buffer));
/* Check the buffer-load against the test string */
g_assert (memcmp (buffer, test_string + test_string_offset, length_read) == 0);
test_string_offset += length_read;
/* Check the seek offset again */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
if (length_read < (gssize) sizeof (buffer)) {
/* Done */
break;
}
}
g_free (test_string);
/* Check the seek offset is within one buffer-load of the end */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), >, test_string_length - sizeof (buffer));
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), <=, test_string_length);
/* Close the stream */
success = g_input_stream_close (download_stream, NULL, &error);
g_assert_no_error (error);
g_assert (success == TRUE);
/* Kill the server and wait for it to die */
stop_server (server, main_loop);
g_thread_join (thread);
g_object_unref (download_stream);
g_object_unref (server);
g_main_loop_unref (main_loop);
}
/* Test seeking forwards after the first read */
static void
test_download_stream_download_seek_after_start_forwards (void)
{
SoupServer *server;
GMainLoop *main_loop;
GThread *thread;
gchar *download_uri, *test_string;
goffset test_string_offset = 0;
guint test_string_length;
GDataService *service;
GInputStream *download_stream;
gssize length_read;
guint8 buffer[20];
gboolean success;
GError *error = NULL;
/* Create and run the server */
server = create_server ((SoupServerCallback) test_download_stream_download_server_seek_handler_cb, NULL, &main_loop);
thread = run_server (server, main_loop);
/* Create a new download stream connected to the server */
download_uri = build_server_uri (server);
service = GDATA_SERVICE (gdata_youtube_service_new ("developer-key", NULL));
download_stream = gdata_download_stream_new (service, NULL, download_uri, NULL);
g_object_unref (service);
g_free (download_uri);
/* Read alternating blocks into a string and compare with what we expect as we go. i.e. Read 20 bytes, then skip 20 bytes, etc. */
test_string = get_test_string (1, 1000);
test_string_length = strlen (test_string) + 1;
while (TRUE) {
/* Check the seek offset */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
/* Read a buffer-load */
length_read = g_input_stream_read (download_stream, buffer, sizeof (buffer), NULL, &error);
g_assert_no_error (error);
g_assert_cmpint (length_read, <=, sizeof (buffer));
/* Check the buffer-load against the test string */
g_assert (memcmp (buffer, test_string + test_string_offset, length_read) == 0);
test_string_offset += length_read;
/* Check the seek offset again */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
if (length_read < (gssize) sizeof (buffer)) {
/* Done */
break;
}
/* Seek forward a buffer length */
if (g_seekable_seek (G_SEEKABLE (download_stream), sizeof (buffer), G_SEEK_CUR, NULL, &error) == FALSE) {
/* Tried to seek past the end of the stream */
g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT);
g_clear_error (&error);
break;
}
test_string_offset += sizeof (buffer);
g_assert_no_error (error);
/* Check the seek offset again */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
}
g_free (test_string);
/* Check the seek offset is within one buffer-load of the end */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), >, test_string_length - sizeof (buffer));
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), <=, test_string_length);
/* Close the stream */
success = g_input_stream_close (download_stream, NULL, &error);
g_assert_no_error (error);
g_assert (success == TRUE);
/* Kill the server and wait for it to die */
stop_server (server, main_loop);
g_thread_join (thread);
g_object_unref (download_stream);
g_object_unref (server);
g_main_loop_unref (main_loop);
}
/* Test seeking backwards after the first read */
static void
test_download_stream_download_seek_after_start_backwards (void)
{
SoupServer *server;
GMainLoop *main_loop;
GThread *thread;
gchar *download_uri, *test_string;
goffset test_string_offset = 0;
guint repeat_count;
GDataService *service;
GInputStream *download_stream;
gssize length_read;
guint8 buffer[20];
gboolean success;
GError *error = NULL;
/* Create and run the server */
server = create_server ((SoupServerCallback) test_download_stream_download_server_seek_handler_cb, NULL, &main_loop);
thread = run_server (server, main_loop);
/* Create a new download stream connected to the server */
download_uri = build_server_uri (server);
service = GDATA_SERVICE (gdata_youtube_service_new ("developer-key", NULL));
download_stream = gdata_download_stream_new (service, NULL, download_uri, NULL);
g_object_unref (service);
g_free (download_uri);
/* Read a block in, then skip back over the block again. i.e. Read the first block, read the second block, skip back over the second block,
* read the second block again, skip back over it, etc. Close the stream after doing this several times. */
test_string = get_test_string (1, 1000);
/* Read a buffer-load to begin with */
length_read = g_input_stream_read (download_stream, buffer, sizeof (buffer), NULL, &error);
g_assert_no_error (error);
test_string_offset += length_read;
for (repeat_count = 6; repeat_count > 0; repeat_count--) {
/* Check the seek offset */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
/* Read a buffer-load */
length_read = g_input_stream_read (download_stream, buffer, sizeof (buffer), NULL, &error);
g_assert_no_error (error);
g_assert_cmpint (length_read, <=, sizeof (buffer));
/* Check the buffer-load against the test string */
g_assert (memcmp (buffer, test_string + test_string_offset, length_read) == 0);
test_string_offset += length_read;
/* Check the seek offset again */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
/* Seek backwards a buffer length */
success = g_seekable_seek (G_SEEKABLE (download_stream), -length_read, G_SEEK_CUR, NULL, &error);
g_assert_no_error (error);
g_assert (success == TRUE);
test_string_offset -= length_read;
/* Check the seek offset again */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
}
g_free (test_string);
/* Check the seek offset is at the end of the first buffer-load */
g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, sizeof (buffer));
/* Close the stream */
success = g_input_stream_close (download_stream, NULL, &error);
g_assert_no_error (error);
g_assert (success == TRUE);
/* Kill the server and wait for it to die */
stop_server (server, main_loop);
g_thread_join (thread);
g_object_unref (download_stream);
g_object_unref (server);
g_main_loop_unref (main_loop);
}
static void
test_upload_stream_upload_no_entry_content_length_server_handler_cb (SoupServer *server, SoupMessage *message, const char *path, GHashTable *query,
SoupClientContext *client, gpointer user_data)
{
gchar *test_string;
goffset test_string_length;
/* Check the Slug and Content-Type headers have been correctly set by the client */
g_assert_cmpstr (soup_message_headers_get_content_type (message->request_headers, NULL), ==, "text/plain");
g_assert_cmpstr (soup_message_headers_get_one (message->request_headers, "Slug"), ==, "slug");
/* Check the client sent the right data */
test_string = get_test_string (1, 1000);
test_string_length = strlen (test_string) + 1;
g_assert_cmpint (message->request_body->length, ==, test_string_length);
g_assert_cmpstr (message->request_body->data, ==, test_string);
g_free (test_string);
/* Add some response headers */
soup_message_set_status (message, SOUP_STATUS_OK);
soup_message_headers_set_content_type (message->response_headers, "text/plain", NULL);
/* Set the response body */
soup_message_body_append (message->response_body, SOUP_MEMORY_STATIC, "Test passed!", 13);
}
static void
test_upload_stream_upload_no_entry_content_length (void)
{
SoupServer *server;
GMainLoop *main_loop;
GThread *thread;
gchar *upload_uri, *test_string;
GDataService *service;
GOutputStream *upload_stream;
gssize length_written;
gsize total_length_written = 0;
goffset test_string_length;
gboolean success;
GError *error = NULL;
/* Create and run the server */
server = create_server ((SoupServerCallback) test_upload_stream_upload_no_entry_content_length_server_handler_cb, NULL, &main_loop);
thread = run_server (server, main_loop);
/* Create a new upload stream uploading to the server */
upload_uri = build_server_uri (server);
service = GDATA_SERVICE (gdata_youtube_service_new ("developer-key", NULL));
upload_stream = gdata_upload_stream_new (service, NULL, SOUP_METHOD_POST, upload_uri, NULL, "slug", "text/plain", NULL);
g_object_unref (service);
g_free (upload_uri);
/* Write the entire test string to the stream */
test_string = get_test_string (1, 1000);
test_string_length = strlen (test_string) + 1;
while ((length_written = g_output_stream_write (upload_stream, test_string + total_length_written,
test_string_length - total_length_written, NULL, &error)) > 0) {
g_assert_cmpint (length_written, <=, test_string_length - total_length_written);
total_length_written += length_written;
}
g_free (test_string);
/* Check we've had a successful return value */
g_assert_no_error (error);
g_assert_cmpint (length_written, ==, 0);
g_assert_cmpint (total_length_written, ==, test_string_length);
/* Close the stream */
success = g_output_stream_close (upload_stream, NULL, &error);
g_assert_no_error (error);
g_assert (success == TRUE);
/* Kill the server and wait for it to die */
stop_server (server, main_loop);
g_thread_join (thread);
g_object_unref (upload_stream);
g_object_unref (server);
g_main_loop_unref (main_loop);
}
/* Test parameters for a run of test_upload_stream_resumable(). */
typedef struct {
enum {
CONTENT_ONLY = 0,
CONTENT_AND_METADATA = 1,
METADATA_ONLY = 2,
} content_type;
#define UPLOAD_STREAM_RESUMABLE_MAX_CONTENT_TYPE 2
gsize file_size;
enum {
ERROR_ON_INITIAL_REQUEST = 0,
ERROR_ON_SUBSEQUENT_REQUEST = 1,
ERROR_ON_FINAL_REQUEST = 2,
NO_ERROR = 3,
} error_type;
#define UPLOAD_STREAM_RESUMABLE_MAX_ERROR_TYPE 3
} UploadStreamResumableTestParams;
static const gchar *upload_stream_resumable_content_type_names[] = {
"content-only",
"content-and-metadata",
"metadata-only",
};
static const gchar *upload_stream_resumable_error_type_names[] = {
"initial-error",
"subsequent-error",
"final-error",
"success",
};
typedef struct {
UploadStreamResumableTestParams *test_params;
gsize next_range_start;
gsize next_range_end;
guint next_path_index;
const gchar *test_string;
} UploadStreamResumableServerData;
static void
test_upload_stream_resumable_server_handler_cb (SoupServer *server, SoupMessage *message, const char *path, GHashTable *query,
SoupClientContext *client, UploadStreamResumableServerData *server_data)
{
UploadStreamResumableTestParams *test_params;
test_params = server_data->test_params;
/* Are we handling the initial request, or a subsequent one? */
if (strcmp (path, "/") == 0) {
/* Initial request. */
gchar *file_size_str;
/* Check the Slug and X-Upload-* headers. */
g_assert_cmpstr (soup_message_headers_get_one (message->request_headers, "Slug"), ==, "slug");
file_size_str = g_strdup_printf ("%" G_GSIZE_FORMAT, test_params->file_size);
g_assert_cmpstr (soup_message_headers_get_one (message->request_headers, "X-Upload-Content-Type"), ==, "text/plain");
g_assert_cmpstr (soup_message_headers_get_one (message->request_headers, "X-Upload-Content-Length"), ==, file_size_str);
g_free (file_size_str);
/* Check the Content-Type and content. */
switch (test_params->content_type) {
case CONTENT_ONLY:
/* Check nothing was sent. */
g_assert_cmpstr (soup_message_headers_get_content_type (message->request_headers, NULL), ==, NULL);
g_assert_cmpint (message->request_body->length, ==, 0);
break;
case CONTENT_AND_METADATA:
case METADATA_ONLY:
/* Check the JSON sent by the client. */
g_assert_cmpstr (soup_message_headers_get_content_type (message->request_headers, NULL), ==, "application/json");
g_assert (message->request_body->data[message->request_body->length] == '\0');
g_assert (gdata_test_compare_json_strings (message->request_body->data,
"{"
"'title': 'Test title!',"
"'kind': 'youtube#video',"
"'snippet': {"
"'title': 'Test title!'"
"},"
"'status': {"
"'privacyStatus': 'public'"
"},"
"'recordingDetails': {}"
"}", TRUE) == TRUE);
break;
default:
g_assert_not_reached ();
}
/* Send a response. */
switch (test_params->error_type) {
case ERROR_ON_INITIAL_REQUEST:
/* Error. */
goto error;
case ERROR_ON_SUBSEQUENT_REQUEST:
case ERROR_ON_FINAL_REQUEST:
case NO_ERROR:
/* Success. */
if (test_params->file_size == 0) {
goto completion;
} else {
goto continuation;
}
break;
default:
g_assert_not_reached ();
}
} else if (*path == '/' && g_ascii_strtoull (path + 1, NULL, 10) == server_data->next_path_index) {
/* Subsequent request. */
/* Check the Slug and X-Upload-* headers. */
g_assert_cmpstr (soup_message_headers_get_one (message->request_headers, "Slug"), ==, NULL);
g_assert_cmpstr (soup_message_headers_get_one (message->request_headers, "X-Upload-Content-Type"), ==, NULL);
g_assert_cmpstr (soup_message_headers_get_one (message->request_headers, "X-Upload-Content-Length"), ==, NULL);
/* Check the Content-Type and content. */
switch (test_params->content_type) {
case CONTENT_ONLY:
case CONTENT_AND_METADATA: {
goffset range_start, range_end, range_length;
/* Check the headers. */
g_assert_cmpstr (soup_message_headers_get_content_type (message->request_headers, NULL), ==, "text/plain");
g_assert_cmpint (soup_message_headers_get_content_length (message->request_headers), ==, message->request_body->length);
g_assert_cmpint (message->request_body->length, >, 0);
g_assert_cmpint (message->request_body->length, <=, 512 * 1024 /* 512 KiB */);
g_assert (soup_message_headers_get_content_range (message->request_headers, &range_start, &range_end,
&range_length) == TRUE);
g_assert_cmpint (range_start, ==, server_data->next_range_start);
g_assert_cmpint (range_end, ==, server_data->next_range_end);
g_assert_cmpint (range_length, ==, test_params->file_size);
/* Check the content. */
g_assert (memcmp (server_data->test_string + range_start, message->request_body->data,
message->request_body->length) == 0);
/* Update the expected values. */
server_data->next_range_start = range_end + 1;
server_data->next_range_end = MIN (server_data->next_range_start + 512 * 1024, test_params->file_size) - 1;
server_data->next_path_index++;
break;
}
case METADATA_ONLY:
default:
g_assert_not_reached ();
}
/* Send a response. */
switch (test_params->error_type) {
case ERROR_ON_SUBSEQUENT_REQUEST:
case ERROR_ON_FINAL_REQUEST:
/* Skip the error if this isn't the final request. */
if (test_params->error_type == ERROR_ON_SUBSEQUENT_REQUEST ||
(test_params->error_type == ERROR_ON_FINAL_REQUEST && server_data->next_range_start == test_params->file_size)) {
goto error;
}
/* Fall through */
case NO_ERROR:
/* Success. */
if (server_data->next_range_start == test_params->file_size) {
goto completion;
} else {
goto continuation;
}
break;
case ERROR_ON_INITIAL_REQUEST:
default:
g_assert_not_reached ();
}
} else {
g_assert_not_reached ();
}
return;
error: {
const gchar *error_response =
"{"
"'error': {"
"'errors': ["
"{"
"'domain': 'global',"
"'reason': 'authError',"
"'message': 'Invalid token.',"
"'location': 'Authorization: GoogleLogin'"
"}"
"],"
"'code': 400,"
"'message': 'Invalid token.'"
"}"
"}";
/* Error. */
soup_message_set_status (message, SOUP_STATUS_UNAUTHORIZED); /* arbitrary error status code */
soup_message_body_append (message->response_body, SOUP_MEMORY_STATIC, error_response, strlen (error_response));
}
return;
continuation: {
gchar *upload_uri, *server_uri;
/* Continuation. */
if (server_data->next_path_index == 0) {
soup_message_set_status (message, SOUP_STATUS_OK);
} else {
soup_message_set_status (message, 308);
}
server_uri = build_server_uri (server);
g_assert_cmpstr (server_uri + strlen (server_uri) - 1, ==, "/");
upload_uri = g_strdup_printf ("%s%u", server_uri,
++server_data->next_path_index);
soup_message_headers_replace (message->response_headers, "Location", upload_uri);
g_free (upload_uri);
g_free (server_uri);
}
return;
completion: {
const gchar *completion_response =
"{"
"'kind': 'youtube#video',"
"'snippet': {"
"'title': 'Test title!',"
"'categoryId': '10'" /* Music */
"},"
"'status': {"
"'privacyStatus': 'public'"
"},"
"'recordingDetails': {"
"'recordingDate': '2005-10-02'"
"}"
"}";
/* Completion. */
soup_message_set_status (message, SOUP_STATUS_CREATED);
soup_message_headers_set_content_type (message->response_headers, "application/json", NULL);
soup_message_body_append (message->response_body, SOUP_MEMORY_STATIC, completion_response, strlen (completion_response));
}
}
static void
test_upload_stream_resumable (gconstpointer user_data)
{
UploadStreamResumableTestParams *test_params;
UploadStreamResumableServerData server_data;
SoupServer *server;
GMainLoop *main_loop;
GThread *thread;
gchar *upload_uri;
GDataService *service;
GDataEntry *entry = NULL;
GOutputStream *upload_stream;
gssize length_written;
gsize total_length_written = 0;
gchar *test_string;
goffset test_string_length;
gboolean success;
GError *error = NULL;
test_params = (UploadStreamResumableTestParams*) user_data;
/* Build the test string. */
if (test_params->file_size > 0) {
test_string = get_test_string (1, test_params->file_size / 4 /* arbitrary number which should generate enough data */);
g_assert (strlen (test_string) + 1 >= test_params->file_size);
test_string[test_params->file_size - 1] = '\0'; /* trim the string to the right length */
} else {
test_string = NULL;
}
test_string_length = test_params->file_size;
/* Create and run the server */
server_data.test_params = test_params;
server_data.next_range_start = 0;
server_data.next_range_end = MIN (test_params->file_size, 512 * 1024 /* 512 KiB */) - 1;
server_data.next_path_index = 0;
server_data.test_string = test_string;
server = create_server ((SoupServerCallback) test_upload_stream_resumable_server_handler_cb, &server_data, &main_loop);
thread = run_server (server, main_loop);
/* Create a new upload stream uploading to the server */
if (test_params->content_type == CONTENT_AND_METADATA || test_params->content_type == METADATA_ONLY) {
/* Build a test entry. */
entry = GDATA_ENTRY (gdata_youtube_video_new (NULL));
gdata_entry_set_title (entry, "Test title!");
}
upload_uri = build_server_uri (server);
service = GDATA_SERVICE (gdata_youtube_service_new ("developer-key", NULL));
upload_stream = gdata_upload_stream_new_resumable (service, NULL, SOUP_METHOD_POST, upload_uri, entry, "slug", "text/plain",
test_params->file_size, NULL);
g_object_unref (service);
g_free (upload_uri);
g_clear_object (&entry);
if (test_params->file_size > 0) {
while ((length_written = g_output_stream_write (upload_stream, test_string + total_length_written,
test_string_length - total_length_written, NULL, &error)) > 0) {
g_assert_cmpint (length_written, <=, test_string_length - total_length_written);
total_length_written += length_written;
}
} else {
/* Do an empty write to poke things into action. */
if ((length_written = g_output_stream_write (upload_stream, "", 0, NULL, &error)) > 0) {
total_length_written += length_written;
}
}
/* Check the return value. */
switch (test_params->error_type) {
case ERROR_ON_INITIAL_REQUEST:
case ERROR_ON_SUBSEQUENT_REQUEST:
case ERROR_ON_FINAL_REQUEST:
/* We can't check the write() call for errors, since whether it throws an error depends on whether the range it's writing
* overlaps a resumable upload chunk, which is entirely arbitrary and unpredictable. */
g_assert_cmpint (length_written, <=, 0);
g_assert_cmpint (total_length_written, <=, test_string_length);
g_clear_error (&error);
/* Close the stream */
success = g_output_stream_close (upload_stream, NULL, &error);
g_assert_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED);
g_assert (success == FALSE);
g_clear_error (&error);
break;
case NO_ERROR:
/* Check we've had a successful return value */
g_assert_no_error (error);
g_assert_cmpint (length_written, ==, 0);
g_assert_cmpint (total_length_written, ==, test_string_length);
/* Close the stream */
success = g_output_stream_close (upload_stream, NULL, &error);
g_assert_no_error (error);
g_assert (success == TRUE);
break;
default:
g_assert_not_reached ();
}
/* Kill the server and wait for it to die */
stop_server (server, main_loop);
g_thread_join (thread);
g_free (test_string);
g_object_unref (upload_stream);
g_object_unref (server);
g_main_loop_unref (main_loop);
}
int
main (int argc, char *argv[])
{
gdata_test_init (argc, argv);
/* Only print out headers, since we're sending a lot of data. */
g_setenv ("LIBGDATA_DEBUG", "2" /* GDATA_LOG_HEADERS */, TRUE);
g_test_add_func ("/download-stream/download_content_length", test_download_stream_download_content_length);
g_test_add_func ("/download-stream/download_seek/before_start", test_download_stream_download_seek_before_start);
g_test_add_func ("/download-stream/download_seek/after_start_forwards", test_download_stream_download_seek_after_start_forwards);
g_test_add_func ("/download-stream/download_seek/after_start_backwards", test_download_stream_download_seek_after_start_backwards);
g_test_add_func ("/upload-stream/upload_no_entry_content_length", test_upload_stream_upload_no_entry_content_length);
/* Test all possible combinations of conditions for resumable uploads. */
{
guint i, j, k;
const gsize file_sizes[] = { /* all in bytes */
407 * 1024, /* < 512 KiB */
512 * 1024, /* 512 KiB */
666 * 1024, /* > 512 KiB, < 1024 KiB */
1024 * 1024, /* 1024 KiB */
1025 * 1024, /* > 1024 KiB */
};
for (i = 0; i < UPLOAD_STREAM_RESUMABLE_MAX_CONTENT_TYPE + 1; i++) {
for (j = 0; j < G_N_ELEMENTS (file_sizes); j++) {
for (k = 0; k < UPLOAD_STREAM_RESUMABLE_MAX_ERROR_TYPE + 1; k++) {
UploadStreamResumableTestParams *test_params;
gchar *test_name;
gsize file_size;
/* Ignore combinations of METADATA_ONLY with file_sizes or non-initial errors. */
if (i == METADATA_ONLY && (j != 0 || k != 0)) {
continue;
} else if (i == METADATA_ONLY) {
file_size = 0 /* bytes */;
} else {
file_size = file_sizes[j];
}
test_name = g_strdup_printf ("/upload-stream/resumable/%s/%s/%" G_GSIZE_FORMAT,
upload_stream_resumable_content_type_names[i],
upload_stream_resumable_error_type_names[k],
file_size);
/* Allocate a new struct. We leak this. */
test_params = g_slice_new (UploadStreamResumableTestParams);
test_params->content_type = i;
test_params->file_size = file_size;
test_params->error_type = k;
g_test_add_data_func (test_name, test_params, test_upload_stream_resumable);
g_free (test_name);
}
}
}
}
return g_test_run ();
}