diff --git a/radicale/storage/filesystem.py b/radicale/storage/filesystem.py index c6efa45..a08c0ff 100644 --- a/radicale/storage/filesystem.py +++ b/radicale/storage/filesystem.py @@ -35,7 +35,7 @@ FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder")) try: from dulwich.repo import Repo - GIT_REPOSITORY = Repo(os.path.join(FOLDER, ".git")) + GIT_REPOSITORY = Repo(FOLDER) except: GIT_REPOSITORY = None @@ -52,7 +52,7 @@ def open(path, mode="r"): # On exit if GIT_REPOSITORY and mode == "w": path = os.path.relpath(abs_path, FOLDER) - GIT_REPOSITORY.stage([path]) + GIT_REPOSITORY.stage([path.encode("utf-8")]) GIT_REPOSITORY.do_commit("Commit by Radicale") # pylint: enable=W0622 diff --git a/schema.sql b/schema.sql index 17619d5..c748e5f 100644 --- a/schema.sql +++ b/schema.sql @@ -1,7 +1,5 @@ -- This is the database schema for PostgreSQL. -begin; - create table collection ( path varchar primary key not null, parent_path varchar references collection (path)); @@ -22,12 +20,10 @@ create table line ( value varchar not null, item_name varchar references item (name) not null, timestamp timestamp not null, - primary key (key, item_name)); + primary key (key, value, item_name, timestamp)); create table property ( key varchar not null, value varchar not null, collection_path varchar references collection (path) not null, primary key (key, collection_path)); - -commit; diff --git a/tests/__init__.py b/tests/__init__.py index 6ef1413..ec5a84d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -21,25 +21,29 @@ Tests for Radicale. """ +import base64 +import hashlib import os +import shutil import sys +import tempfile +from dulwich.repo import Repo +from io import BytesIO sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) import radicale +from radicale import config +from radicale.auth import htpasswd +from radicale.storage import filesystem, database +from .helpers import get_file_content +from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine class BaseTest(object): """Base class for tests.""" - - def setup(self): - """Setup function for each test.""" - self.application = radicale.Application() - - def teardown(self): - """Teardown function for each test.""" - - def request(self, method, path, **args): + def request(self, method, path, data=None, **args): """Send a request.""" self.application._status = None self.application._headers = None @@ -49,14 +53,86 @@ class BaseTest(object): args[key.upper()] = args[key] args["REQUEST_METHOD"] = method.upper() args["PATH_INFO"] = path + if data: + args["wsgi.input"] = BytesIO(data) + args["CONTENT_LENGTH"] = str(len(data)) self.application._answer = self.application(args, self.start_response) return ( int(self.application._status.split()[0]), dict(self.application._headers), - self.application._answer[0].decode("utf-8")) + self.application._answer[0].decode("utf-8") + if self.application._answer else None) def start_response(self, status, headers): """Put the response values into the current application.""" self.application._status = status self.application._headers = headers + + +class FileSystem(BaseTest): + """Base class for filesystem tests.""" + storage_type = "filesystem" + + def setup(self): + """Setup function for each test.""" + self.colpath = tempfile.mkdtemp() + config.set("storage", "type", self.storage_type) + filesystem.FOLDER = self.colpath + filesystem.GIT_REPOSITORY = None + self.application = radicale.Application() + + def teardown(self): + """Teardown function for each test.""" + shutil.rmtree(self.colpath) + + +class MultiFileSystem(FileSystem): + """Base class for multifilesystem tests.""" + storage_type = "multifilesystem" + + +class DataBaseSystem(BaseTest): + """Base class for database tests""" + def setup(self): + config.set("storage", "type", "database") + config.set("storage", "database_url", "sqlite://") + database.Session = sessionmaker() + database.Session.configure(bind=create_engine("sqlite://")) + session = database.Session() + # session.execute(get_file_content("schema.sql")) + for st in get_file_content("schema.sql").split(";"): + session.execute(st) + session.commit() + self.application = radicale.Application() + + +class GitFileSystem(FileSystem): + """Base class for filesystem tests using Git""" + def setup(self): + super(GitFileSystem, self).setup() + Repo.init(self.colpath) + filesystem.GIT_REPOSITORY = Repo(self.colpath) + + +class GitMultiFileSystem(GitFileSystem, MultiFileSystem): + """Base class for multifilesystem tests using Git""" + + +class HtpasswdAuthSystem(BaseTest): + """Base class to test Radicale with Htpasswd authentication""" + def setup(self): + self.colpath = tempfile.mkdtemp() + htpasswd_file_path = os.path.join(self.colpath, ".htpasswd") + with open(htpasswd_file_path, "w") as fd: + fd.write('tmp:{SHA}' + base64.b64encode( + hashlib.sha1("bépo").digest())) + config.set("auth", "type", "htpasswd") + self.userpass = base64.b64encode("tmp:bépo") + self.application = radicale.Application() + htpasswd.FILENAME = htpasswd_file_path + htpasswd.ENCRYPTION = "sha1" + + def teardown(self): + config.set("auth", "type", "None") + radicale.auth.is_authenticated = lambda *_: True diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..b8b4c7d --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 Nicolas Kandel +# Copyright © 2008 Pascal Halter +# Copyright © 2008-2013 Guillaume Ayoub +# +# This library 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 library 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 Radicale. If not, see . + +""" +Radicale Helpers module. + +This module offers helpers to use in tests. + +""" + +import os + +EXAMPLES_FOLDER = os.path.join(os.path.dirname(__file__), "static") + + +def get_file_content(file_name): + try: + with open(os.path.join(EXAMPLES_FOLDER, file_name)) as fd: + return fd.read() + except IOError: + print(u"Couldn't open the file %s" % file_name) diff --git a/tests/static/put.ics b/tests/static/put.ics new file mode 100644 index 0000000..45f6a97 --- /dev/null +++ b/tests/static/put.ics @@ -0,0 +1,31 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/Paris +X-LIC-LOCATION:Europe/Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20130902T150157Z +LAST-MODIFIED:20130902T150158Z +DTSTAMP:20130902T150158Z +UID:02805f81-4cc2-4d68-8d39-72768ffa02d9 +SUMMARY:Nouvel évènement +DTSTART;TZID=Europe/Paris:20130902T180000 +DTEND;TZID=Europe/Paris:20130902T190000 +END:VEVENT +END:VCALENDAR diff --git a/tests/static/putvtodo.ics b/tests/static/putvtodo.ics new file mode 100644 index 0000000..8c74fdb --- /dev/null +++ b/tests/static/putvtodo.ics @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTODO +CREATED:20130903T091105Z +LAST-MODIFIED:20130903T091108Z +DTSTAMP:20130903T091108Z +UID:40f8cf9b-0e62-4624-89a2-24c5e68850f5 +SUMMARY:Nouvelle tâche +END:VTODO +END:VCALENDAR diff --git a/tests/static/schema.sql b/tests/static/schema.sql new file mode 120000 index 0000000..50d2d72 --- /dev/null +++ b/tests/static/schema.sql @@ -0,0 +1 @@ +../../schema.sql \ No newline at end of file diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..4c5a550 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2012-2013 Guillaume Ayoub +# Copyright © 2012-2013 Jean-Marc Martins +# +# This library 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 library 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 Radicale. If not, see . + +""" +Radicale tests with simple requests and authentication. + +""" + +from nose import with_setup +from . import HtpasswdAuthSystem + + +class TestBaseAuthRequests(HtpasswdAuthSystem): + """ + Tests basic requests with auth. + + ..note Only htpasswd works at the moment since + it requires to spawn processes running servers for + others auth methods (ldap). + """ + + @with_setup(HtpasswdAuthSystem.setup, HtpasswdAuthSystem.teardown) + def test_root(self): + """Tests a GET request at "/".""" + status, headers, answer = self.request( + "GET", "/", HTTP_AUTHORIZATION=self.userpass) + assert status == 200 + assert "Radicale works!" in answer diff --git a/tests/test_base.py b/tests/test_base.py index dfc375a..542441b 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -21,10 +21,13 @@ Radicale tests with simple requests. """ -from . import BaseTest +from . import (FileSystem, MultiFileSystem, DataBaseSystem, + GitFileSystem, GitMultiFileSystem) +from .helpers import get_file_content +import sys -class TestBaseRequests(BaseTest): +class BaseRequests(object): """Tests with simple requests.""" def test_root(self): @@ -32,3 +35,56 @@ class TestBaseRequests(BaseTest): status, headers, answer = self.request("GET", "/") assert status == 200 assert "Radicale works!" in answer + # Tests the creation of the collection + status, headers, answer = self.request("GET", "/calendar.ics/") + assert u"BEGIN:VCALENDAR" in answer + assert u"VERSION:2.0" in answer + assert u"END:VCALENDAR" in answer + assert u"PRODID:-//Radicale//NONSGML Radicale Server//EN" in answer + + def test_add_event_todo(self): + """Tests the add of an event and todo.""" + self.request("GET", "/calendar.ics/") + #VEVENT test + event = get_file_content("put.ics") + path = "/calendar.ics/02805f81-4cc2-4d68-8d39-72768ffa02d9.ics" + status, headers, answer = self.request("PUT", path, event) + assert status == 201 + assert u"ETag" in headers.keys() + status, headers, answer = self.request("GET", path) + assert status == 200 + assert u"VEVENT" in answer + assert u"Nouvel évènement" in answer + assert u"UID:02805f81-4cc2-4d68-8d39-72768ffa02d9" in answer + # VTODO test + todo = get_file_content("putvtodo.ics") + path = "/calendar.ics/40f8cf9b-0e62-4624-89a2-24c5e68850f5.ics" + status, headers, answer = self.request("PUT", path, todo) + assert status == 201 + assert u"ETag" in headers.keys() + status, headers, answer = self.request("GET", path) + assert u"VTODO" in answer + assert u"Nouvelle tâche" in answer + assert u"UID:40f8cf9b-0e62-4624-89a2-24c5e68850f5" in answer + + def test_delete(self): + """Tests the deletion of an event""" + self.request("GET", "/calendar.ics/") + # Adds a VEVENT to be deleted + event = get_file_content("put.ics") + path = "/calendar.ics/02805f81-4cc2-4d68-8d39-72768ffa02d9.ics" + status, headers, answer = self.request("PUT", path, event) + # Then we send a DELETE request + status, headers, answer = self.request("DELETE", path) + assert status == 200 + assert u"%s" % path in answer + status, headers, answer = self.request("GET", "/calendar.ics/") + assert u"VEVENT" not in answer + +# Generates Classes with different configs +cl_list = [FileSystem, MultiFileSystem, DataBaseSystem, + GitFileSystem, GitMultiFileSystem] +for cl in cl_list: + classname = "Test%s" % cl.__name__ + setattr(sys.modules[__name__], + classname, type(classname, (BaseRequests, cl), {}))