diff --git a/tests/calendar.ics b/tests/calendar.ics new file mode 100644 index 00000000..30d98d8f --- /dev/null +++ b/tests/calendar.ics @@ -0,0 +1,32 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VEVENT +SUMMARY:Access-A-Ride Pickup +DTSTART;TZID=America/New_York:20130802T103400 +DTEND;TZID=America/New_York:20130802T110400 +LOCATION:1000 Broadway Ave.\, Brooklyn +DESCRIPTION: Access-A-Ride trip to 900 Jay St.\, Brooklyn +STATUS:CONFIRMED +SEQUENCE:3 +BEGIN:VALARM +TRIGGER:-PT10M +DESCRIPTION:Pickup Reminder +ACTION:DISPLAY +END:VALARM +END:VEVENT +BEGIN:VEVENT +SUMMARY:Access-A-Ride Pickup +DTSTART;TZID=America/New_York:20130802T200000 +DTEND;TZID=America/New_York:20130802T203000 +LOCATION:900 Jay St.\, Brooklyn +DESCRIPTION: Access-A-Ride trip to 1000 Broadway Ave.\, Brooklyn +STATUS:CONFIRMED +SEQUENCE:3 +BEGIN:VALARM +TRIGGER:-PT10M +DESCRIPTION:Pickup Reminder +ACTION:DISPLAY +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/tests/gcal-simple-server.c b/tests/gcal-simple-server.c new file mode 100644 index 00000000..21ad7db8 --- /dev/null +++ b/tests/gcal-simple-server.c @@ -0,0 +1,330 @@ +/* gcal-simple-server.c + * + * Copyright 2019 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GcalSimpleServer" + +#include "gcal-simple-server.h" + +struct _GcalSimpleServer +{ + GObject parent; + + /* Only valid while running */ + GThread *thread; + GMainLoop *thread_mainloop; + SoupServer *server; + gchar *uri; + + GMutex running_mutex; + GCond running_cond; + gboolean running; +}; + +G_DEFINE_TYPE (GcalSimpleServer, gcal_simple_server, G_TYPE_OBJECT) + + +/* + * Auxiliary methods + */ + +static void +process_get (SoupMessage *message, + const gchar *prefix, + const gchar *path) +{ + g_autofree gchar *calendar_path = g_strdup_printf ("%s/calendar", prefix); + g_autofree gchar *calendar_file = g_strdup_printf ("%s/calendar.ics", prefix); + + if (g_strcmp0 (path, calendar_path) == 0) + { + g_debug ("Serving empty calendar"); + soup_message_set_response (message, + "text/calendar", + SOUP_MEMORY_STATIC, + GCAL_TEST_SERVER_EMPTY_CALENDAR, + strlen (GCAL_TEST_SERVER_EMPTY_CALENDAR)); + } + + if (g_strcmp0 (path, calendar_file) == 0) + { + g_autoptr (SoupBuffer) buffer = NULL; + GMappedFile *mapping; + + g_debug ("Serving calendar.ics"); + + mapping = g_mapped_file_new (path, FALSE, NULL); + if (!mapping) + { + soup_message_set_status (message, SOUP_STATUS_INTERNAL_SERVER_ERROR); + return; + } + + buffer = soup_buffer_new_with_owner (g_mapped_file_get_contents (mapping), + g_mapped_file_get_length (mapping), + mapping, + (GDestroyNotify)g_mapped_file_unref); + soup_message_body_append_buffer (message->response_body, buffer); + } +} + +static void +process_caldav (SoupMessage *message, + const gchar *path) +{ + g_message ("Processing CalDAV"); +} + + +/* + * Callbacks + */ + +static gboolean +idle_quit_server_cb (gpointer user_data) +{ + GcalSimpleServer *self = user_data; + + g_main_loop_quit (self->thread_mainloop); + + return G_SOURCE_REMOVE; +} + +static void +no_auth_handler_cb (SoupServer *server, + SoupMessage *message, + const gchar *path, + GHashTable *query, + SoupClientContext *client, + gpointer user_data) +{ + g_debug ("No authenticaton needed"); + + if (message->method == SOUP_METHOD_GET) + process_get (message, "/public", path); + else if (message->method == SOUP_METHOD_PROPFIND) + process_caldav (message, path); + else + soup_message_set_status (message, SOUP_STATUS_NOT_IMPLEMENTED); +} + +static void +auth_handler_cb (SoupServer *server, + SoupMessage *message, + const gchar *path, + GHashTable *query, + SoupClientContext *client, + gpointer user_data) +{ + g_debug ("Needs authenticaton"); + + if (message->method == SOUP_METHOD_GET) + process_get (message, "/secret-area", path); + else if (message->method == SOUP_METHOD_PROPFIND) + process_caldav (message, path); + else + soup_message_set_status (message, SOUP_STATUS_NOT_IMPLEMENTED); +} + +static gboolean +authorize_cb (SoupAuthDomain *domain, + SoupMessage *message, + const char *username, + const char *password, + gpointer user_data) +{ + const struct { + const gchar *username; + const gchar *password; + } valid_credentials[] = { + { "unicorn", "iamnotahorse" }, + { "feaneron", "idonotmaintainanything" }, + { "purism", "FINISHTHEPHONE!" }, + }; + guint i; + + g_debug ("Authorizing username: '%s', password: '%s'", username, password); + + if (!username && !password) + return FALSE; + + for (i = 0; i < G_N_ELEMENTS (valid_credentials); i++) + { + if (g_strcmp0 (valid_credentials[i].username, username) == 0 && + g_strcmp0 (valid_credentials[i].password, password) == 0) + { + return TRUE; + } + } + + return FALSE; +} + +static gpointer +run_server_in_thread (gpointer data) +{ + g_autoptr (SoupAuthDomain) domain = NULL; + g_autoptr (GMainContext) context = NULL; + g_autoptr (SoupServer) server = NULL; + g_autoptr (GMainLoop) mainloop = NULL; + g_autoslist (SoupURI) uris = NULL; + g_autoptr (GError) error = NULL; + GcalSimpleServer *self = data; + g_autofree gchar *uri = NULL; + + context = g_main_context_new (); + g_main_context_push_thread_default (context); + mainloop = g_main_loop_new (context, FALSE); + self->thread_mainloop = mainloop; + + server = soup_server_new (SOUP_SERVER_SERVER_HEADER, "gcal-simple-server", + NULL); + + /* Anything under /secret-area and /private requires authentication */ + domain = soup_auth_domain_basic_new (SOUP_AUTH_DOMAIN_REALM, "GcalSimpleServer", + SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK, authorize_cb, + SOUP_AUTH_DOMAIN_BASIC_AUTH_DATA, self, + SOUP_AUTH_DOMAIN_ADD_PATH, "/secret-area", + NULL); + soup_server_add_auth_domain (server, domain); + + soup_server_listen_local (server, 0, SOUP_SERVER_LISTEN_IPV4_ONLY, &error); + if (error) + { + g_critical ("Error starting up server: %s", error->message); + return NULL; + } + + soup_server_add_handler (server, NULL, no_auth_handler_cb, NULL, NULL); + soup_server_add_handler (server, "/public", no_auth_handler_cb, NULL, NULL); + soup_server_add_handler (server, "/secret-area", auth_handler_cb, NULL, NULL); + + uris = soup_server_get_uris (server); + g_assert_cmpint (g_slist_length (uris), ==, 1); + + uri = soup_uri_to_string (uris->data, FALSE); + self->uri = g_strdup (uri); + + g_debug ("Listening on %s", uri); + + g_mutex_lock (&self->running_mutex); + self->running = TRUE; + g_cond_signal (&self->running_cond); + g_mutex_unlock (&self->running_mutex); + + g_main_loop_run (mainloop); + + g_debug ("Stopping"); + + soup_server_disconnect (server); + + g_main_context_pop_thread_default (context); + + return NULL; +} + + +/* + * GObject overrides + */ + +static void +gcal_simple_server_finalize (GObject *object) +{ + GcalSimpleServer *self = (GcalSimpleServer *)object; + + if (self->thread) + gcal_simple_server_stop (self); + + g_clear_pointer (&self->uri, g_free); + + G_OBJECT_CLASS (gcal_simple_server_parent_class)->finalize (object); +} + +static void +gcal_simple_server_class_init (GcalSimpleServerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gcal_simple_server_finalize; +} + +static void +gcal_simple_server_init (GcalSimpleServer *self) +{ +} + +GcalSimpleServer * +gcal_simple_server_new (void) +{ + return g_object_new (GCAL_TYPE_SIMPLE_SERVER, NULL); +} + +void +gcal_simple_server_start (GcalSimpleServer *self) +{ + g_return_if_fail (GCAL_IS_SIMPLE_SERVER (self)); + + if (self->thread) + { + g_warning ("Server already started"); + return; + } + + self->thread = g_thread_new ("gcal_simple_server_thread", run_server_in_thread, self); + + g_mutex_lock (&self->running_mutex); + while (!self->running) + g_cond_wait (&self->running_cond, &self->running_mutex); + g_mutex_unlock (&self->running_mutex); +} + +void +gcal_simple_server_stop (GcalSimpleServer *self) +{ + GMainContext *context; + + g_return_if_fail (GCAL_IS_SIMPLE_SERVER (self)); + + if (!self->thread) + { + g_warning ("Server not running"); + return; + } + + context = g_main_loop_get_context (self->thread_mainloop); + soup_add_completion (context, idle_quit_server_cb, self); + + g_mutex_lock (&self->running_mutex); + self->running = FALSE; + g_cond_signal (&self->running_cond); + g_mutex_unlock (&self->running_mutex); + + g_thread_join (self->thread); + self->thread = NULL; +} + +SoupURI* +gcal_simple_server_get_uri (GcalSimpleServer *self) +{ + g_return_val_if_fail (GCAL_IS_SIMPLE_SERVER (self), NULL); + g_return_val_if_fail (self->thread != NULL, NULL); + + return soup_uri_new (self->uri); +} diff --git a/tests/gcal-simple-server.h b/tests/gcal-simple-server.h new file mode 100644 index 00000000..012041bc --- /dev/null +++ b/tests/gcal-simple-server.h @@ -0,0 +1,45 @@ +/* gcal-simple-server.h + * + * Copyright 2019 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + + +#define GCAL_TEST_SERVER_EMPTY_CALENDAR \ +"BEGIN:VCALENDAR\n" \ +"END:VCALENDAR" + + +#define GCAL_TYPE_SIMPLE_SERVER (gcal_simple_server_get_type()) +G_DECLARE_FINAL_TYPE (GcalSimpleServer, gcal_simple_server, GCAL, SIMPLE_SERVER, GObject) + +GcalSimpleServer* gcal_simple_server_new (void); + +void gcal_simple_server_start (GcalSimpleServer *self); + +void gcal_simple_server_stop (GcalSimpleServer *self); + +SoupURI* gcal_simple_server_get_uri (GcalSimpleServer *self); + +G_END_DECLS diff --git a/tests/meson.build b/tests/meson.build index 940b44b7..a45a00ef 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -13,6 +13,7 @@ test_cflags = '-DTEST_DATA_DIR="@0@"'.format(join_paths(meson.source_root(), 'da ################ sources = files( + 'gcal-simple-server.c', 'gcal-stub-calendar.c', ) @@ -31,6 +32,7 @@ libgcal_test_dep = declare_dependency( ) tests = [ + 'server', 'event', 'manager', ] diff --git a/tests/test-server.c b/tests/test-server.c new file mode 100644 index 00000000..6a8e0f9a --- /dev/null +++ b/tests/test-server.c @@ -0,0 +1,265 @@ +/* test-server.c + * + * Copyright 2019 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include + +#include "gcal-simple-server.h" + +static GcalSimpleServer* +init_server (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + + server = gcal_simple_server_new (); + gcal_simple_server_start (server); + + return g_steal_pointer (&server); +} + +static void +fail_authenticate_cb (SoupSession *session, + SoupMessage *message, + SoupAuth *auth, + gboolean retrying, + gpointer user_data) +{ + GcalSimpleServer *server = user_data; + + gcal_simple_server_stop (server); + + g_assert_not_reached (); +} + +static void +authenticate_cb (SoupSession *session, + SoupMessage *message, + SoupAuth *auth, + gboolean retrying, + gpointer user_data) +{ + g_debug ("Authenticating..."); + + soup_auth_authenticate (auth, "unicorn", "iamnotahorse"); +} + +static void +wrong_authenticate_cb (SoupSession *session, + SoupMessage *message, + SoupAuth *auth, + gboolean retrying, + gpointer user_data) +{ + g_debug ("Authenticating with wrong username..."); + + if (!retrying) + soup_auth_authenticate (auth, "thyartismurder", "popmusic"); +} + +/*********************************************************************************************************************/ + +static void +server_init (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + + server = gcal_simple_server_new (); + gcal_simple_server_start (server); +} + +/*********************************************************************************************************************/ + +static void +server_request_no_auth_empty (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + g_autoptr (SoupMessage) message = NULL; + g_autoptr (SoupSession) session = NULL; + g_autoptr (SoupURI) uri = NULL; + g_autoptr (GError) error = NULL; + + server = init_server (); + uri = gcal_simple_server_get_uri (server); + + session = soup_session_new (); + g_signal_connect (session, "authenticate", G_CALLBACK (fail_authenticate_cb), server); + + message = soup_message_new_from_uri ("GET", uri); + soup_session_send (session, message, NULL, &error); + g_assert_no_error (error); +} + +/*********************************************************************************************************************/ + +static void +server_request_no_auth_ics (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + g_autoptr (SoupMessage) message = NULL; + g_autoptr (SoupSession) session = NULL; + g_autoptr (SoupURI) uri = NULL; + g_autoptr (GError) error = NULL; + + server = init_server (); + uri = gcal_simple_server_get_uri (server); + soup_uri_set_path (uri, "/public/calendar.ics"); + + session = soup_session_new (); + g_signal_connect (session, "authenticate", G_CALLBACK (fail_authenticate_cb), server); + + message = soup_message_new_from_uri ("GET", uri); + soup_session_send (session, message, NULL, &error); + g_assert_no_error (error); +} + +/*********************************************************************************************************************/ + +static void +server_request_no_auth_calendar (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + g_autoptr (SoupMessage) message = NULL; + g_autoptr (SoupSession) session = NULL; + g_autoptr (SoupURI) uri = NULL; + g_autoptr (GError) error = NULL; + + server = init_server (); + uri = gcal_simple_server_get_uri (server); + soup_uri_set_path (uri, "/public/calendar"); + + session = soup_session_new (); + g_signal_connect (session, "authenticate", G_CALLBACK (fail_authenticate_cb), server); + + message = soup_message_new_from_uri ("GET", uri); + soup_session_send (session, message, NULL, &error); + g_assert_no_error (error); +} + +/*********************************************************************************************************************/ + +static void +server_request_auth_empty (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + g_autoptr (SoupMessage) message = NULL; + g_autoptr (SoupSession) session = NULL; + g_autoptr (SoupURI) uri = NULL; + g_autoptr (GError) error = NULL; + + server = init_server (); + uri = gcal_simple_server_get_uri (server); + soup_uri_set_path (uri, "/secret-area"); + + session = soup_session_new (); + g_signal_connect (session, "authenticate", G_CALLBACK (authenticate_cb), server); + + message = soup_message_new_from_uri ("GET", uri); + soup_session_send (session, message, NULL, &error); + g_assert_no_error (error); +} + +/*********************************************************************************************************************/ + +static void +server_request_auth_calendar (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + g_autoptr (SoupMessage) message = NULL; + g_autoptr (SoupSession) session = NULL; + g_autoptr (SoupURI) uri = NULL; + g_autoptr (GError) error = NULL; + + server = init_server (); + uri = gcal_simple_server_get_uri (server); + soup_uri_set_path (uri, "/secret-area/calendar"); + + session = soup_session_new (); + g_signal_connect (session, "authenticate", G_CALLBACK (authenticate_cb), server); + + message = soup_message_new_from_uri ("GET", uri); + soup_session_send (session, message, NULL, &error); + g_assert_no_error (error); +} + +/*********************************************************************************************************************/ + +static void +server_request_auth_ics (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + g_autoptr (SoupMessage) message = NULL; + g_autoptr (SoupSession) session = NULL; + g_autoptr (SoupURI) uri = NULL; + g_autoptr (GError) error = NULL; + + server = init_server (); + uri = gcal_simple_server_get_uri (server); + soup_uri_set_path (uri, "/secret-area/calendar.ics"); + + session = soup_session_new (); + g_signal_connect (session, "authenticate", G_CALLBACK (authenticate_cb), server); + + message = soup_message_new_from_uri ("GET", uri); + soup_session_send (session, message, NULL, &error); + g_assert_no_error (error); +} + +/*********************************************************************************************************************/ + +static void +server_request_auth_wrong (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + g_autoptr (SoupMessage) message = NULL; + g_autoptr (SoupSession) session = NULL; + g_autoptr (SoupURI) uri = NULL; + g_autoptr (GError) error = NULL; + + server = init_server (); + uri = gcal_simple_server_get_uri (server); + soup_uri_set_path (uri, "/secret-area"); + + session = soup_session_new (); + g_signal_connect (session, "authenticate", G_CALLBACK (wrong_authenticate_cb), server); + + message = soup_message_new_from_uri ("GET", uri); + soup_session_send (session, message, NULL, &error); + g_assert_no_error (error); + g_assert_cmpuint (message->status_code, ==, SOUP_STATUS_UNAUTHORIZED); +} + +/*********************************************************************************************************************/ + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/server/init", server_init); + g_test_add_func ("/server/no-auth/empty", server_request_no_auth_empty); + g_test_add_func ("/server/no-auth/calendar", server_request_no_auth_calendar); + g_test_add_func ("/server/no-auth/ics", server_request_no_auth_ics); + g_test_add_func ("/server/auth/empty", server_request_auth_empty); + g_test_add_func ("/server/auth/calendar", server_request_auth_calendar); + g_test_add_func ("/server/auth/ics", server_request_auth_ics); + g_test_add_func ("/server/auth/wrong", server_request_auth_wrong); + + return g_test_run (); +}