diff --git a/ActiveSync/SOGoActiveSyncDispatcher+Sync.m b/ActiveSync/SOGoActiveSyncDispatcher+Sync.m index 0f5283b16..0e2072414 100644 --- a/ActiveSync/SOGoActiveSyncDispatcher+Sync.m +++ b/ActiveSync/SOGoActiveSyncDispatcher+Sync.m @@ -2153,7 +2153,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // Cache-entry still exists but folder doesn't exists or synchronize flag is not set. // We ignore the folder and wait for foldersync to do the cleanup. - if (!(mfCollection && [mfCollection synchronize])) + if (!(mfCollection && [(SOGoGCSFolder*)mfCollection synchronize])) { if (debugOn) [self logWithFormat: @"EAS - Folder %@ not found. Ignoring ...", folderName]; @@ -2187,7 +2187,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. realCollectionId = [folderName realCollectionIdWithFolderType: &mergedFolderType]; mfCollection = [self collectionFromId: realCollectionId type: mergedFolderType]; - if (!(mfCollection && [mfCollection synchronize])) + if (!(mfCollection && [(SOGoGCSFolder*)mfCollection synchronize])) { if (debugOn) [self logWithFormat: @"EAS - Folder %@ not found. Reset personal folder to cleanup", folderName]; diff --git a/ActiveSync/SOGoActiveSyncDispatcher.m b/ActiveSync/SOGoActiveSyncDispatcher.m index 6122a2d0b..31ab9b952 100644 --- a/ActiveSync/SOGoActiveSyncDispatcher.m +++ b/ActiveSync/SOGoActiveSyncDispatcher.m @@ -969,7 +969,7 @@ void handle_eas_terminate(int signum) } // Remove the folder from device if it doesn't exist, or don't want to sync it. - if (!currentFolder || !([currentFolder synchronize])) + if (!currentFolder || !([(SOGoGCSFolder*)currentFolder synchronize])) { // Don't send a delete when MergedFoler is set, we have done it above. // Windows Phones don't like when a -folder is sent twice. @@ -1124,7 +1124,7 @@ void handle_eas_terminate(int signum) continue; if (![currentFolder isKindOfClass: [SOGoGCSFolder class]] || - ![currentFolder synchronize]) + ![(SOGoGCSFolder*)currentFolder synchronize]) { [folders removeObjectAtIndex: count]; continue; diff --git a/CHANGELOG.md b/CHANGELOG.md index d1bff6525..e56c649e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [2.4.2](https://github.com/inverse-inc/sogo/compare/SOGo-2.4.1...SOGo-2.4.2) (2022-04-04) + +### Bug Fixes + +* **calendar(dav):** add DAV:status to DAV:response only when deleted ([9bffee2](https://github.com/inverse-inc/sogo/commit/9bffee269d0479927d32bb8371c732509f062d88)), closes [#5163](https://www.sogo.nu/bugs/view.php?id=5163) +* **calendar(dav):** add method attribute to content-type of iTIP reply ([3e96d68](https://github.com/inverse-inc/sogo/commit/3e96d68f308d59a5497807e29dfc8d7df8f2393b)), closes [#5320](https://www.sogo.nu/bugs/view.php?id=5320) +* **core:** add security flags to cookies (HttpOnly, secure) ([0f3d7dc](https://github.com/inverse-inc/sogo/commit/0f3d7dc6bcb9457e91c93e89def0310e63e81f3d)), closes [#4525](https://www.sogo.nu/bugs/view.php?id=4525) +* **core:** fix GCC 10 compatibility ([dc4fdb2](https://github.com/inverse-inc/sogo/commit/dc4fdb2d5a7c354f72b21ecfbfde306dd2c9c532)), closes [#5029](https://www.sogo.nu/bugs/view.php?id=5029) +* **core:** only escape "%" with the SQL LIKE operator ([2389e44](https://github.com/inverse-inc/sogo/commit/2389e4451376862db3b54e2319716615177ea063)) +* **eas:** gcc v10 compat fixes (fixes [#5029](https://www.sogo.nu/bugs/view.php?id=5029)) ([3d2e5ad](https://github.com/inverse-inc/sogo/commit/3d2e5adee83a87ba5141cc329d8a0ad25a9c2b73)) +* **mail(css):** restrict the viewport of the message body viewer ([e528096](https://github.com/inverse-inc/sogo/commit/e528096b10f2702e5c948a8d1e53169ce65a1466)) +* **mail(html):** ban "javascript:" prefix in href, action and formaction ([dd7dd49](https://github.com/inverse-inc/sogo/commit/dd7dd49641fcf24f58c2edd7ca84f7e061cb32d9)) +* **mail(js):** ban all "on*" events attributes from HTML tags ([f38eded](https://github.com/inverse-inc/sogo/commit/f38eded701fff37f570c91a2c5c62075a0ebe439)) +* **mail:** don't allow XML inline attachments ([3c85dbd](https://github.com/inverse-inc/sogo/commit/3c85dbd74dc845dae8d5e992fb673575e171074b)) + ## [2.4.1](https://github.com/inverse-inc/sogo/compare/SOGo-2.4.0...SOGo-2.4.1) (2021-06-01) ### Bug Fixes diff --git a/Scripts/tmpwatch b/Scripts/tmpwatch index b834a417d..989191b2f 100644 --- a/Scripts/tmpwatch +++ b/Scripts/tmpwatch @@ -3,5 +3,5 @@ # SOGOSPOOL must match the value of the configuration parameter SOGoMailSpoolPath SOGOSPOOL=/var/spool/sogo -/usr/sbin/tmpwatch 24 "$SOGOSPOOL" +find "$SOGOSPOOL" -type f -user sogo -atime +23 -delete > /dev/null find "$SOGOSPOOL" -mindepth 1 -type d -user sogo -empty -delete > /dev/null diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.m b/SoObjects/Appointments/SOGoAppointmentFolder.m index de60c1519..cb22f60cf 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.m +++ b/SoObjects/Appointments/SOGoAppointmentFolder.m @@ -1,5 +1,5 @@ /* - Copyright (C) 2007-2014 Inverse inc. + Copyright (C) 2007-2022 Inverse inc. Copyright (C) 2004-2005 SKYRIX Software AG This file is part of SOGo. @@ -795,7 +795,7 @@ static Class iCalEventK = nil; if ([title length]) [baseWhere addObject: [NSString stringWithFormat: @"c_title isCaseInsensitiveLike: '%%%@%%'", - [title asSafeSQLString]]]; + [title asSafeSQLLikeString]]]; if (component) { @@ -1532,14 +1532,14 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir if ([filters isEqualToString:@"title_Category_Location"] || [filters isEqualToString:@"entireContent"]) { [baseWhere addObject: [NSString stringWithFormat: @"(c_title isCaseInsensitiveLike: '%%%@%%' OR c_category isCaseInsensitiveLike: '%%%@%%' OR c_location isCaseInsensitiveLike: '%%%@%%')", - [title asSafeSQLString], - [title asSafeSQLString], - [title asSafeSQLString]]]; + [title asSafeSQLLikeString], + [title asSafeSQLLikeString], + [title asSafeSQLLikeString]]]; } } else [baseWhere addObject: [NSString stringWithFormat: @"c_title isCaseInsensitiveLike: '%%%@%%'", - [title asSafeSQLString]]]; + [title asSafeSQLLikeString]]]; } /* prepare mandatory fields */ diff --git a/SoObjects/Appointments/SOGoAppointmentObject.m b/SoObjects/Appointments/SOGoAppointmentObject.m index e2145370d..0516bfd9b 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.m +++ b/SoObjects/Appointments/SOGoAppointmentObject.m @@ -2065,9 +2065,8 @@ inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent // else if (scheduling && [event userIsAttendee: ownerUser]) { - [self sendIMIPReplyForEvent: event - from: ownerUser - to: [event organizer]]; + [self sendResponseToOrganizer: event + from: ownerUser]; } [self sendReceiptEmailForObject: event diff --git a/SoObjects/Contacts/SOGoContactGCSFolder.m b/SoObjects/Contacts/SOGoContactGCSFolder.m index 55a49cd42..3a4473cdb 100644 --- a/SoObjects/Contacts/SOGoContactGCSFolder.m +++ b/SoObjects/Contacts/SOGoContactGCSFolder.m @@ -1,5 +1,5 @@ /* - Copyright (C) 2006-2013 Inverse inc. + Copyright (C) 2006-2022 Inverse inc. Copyright (C) 2004-2005 SKYRIX Software AG This file is part of SOGo. @@ -178,7 +178,7 @@ static NSArray *folderListingFields = nil; if ([filter length] > 0) { - filter = [filter asSafeSQLString]; + filter = [filter asSafeSQLLikeString]; if ([criteria isEqualToString: @"name_or_address"]) qs = [NSString stringWithFormat: @"(c_sn isCaseInsensitiveLike: '%%%@%%') OR " @@ -281,7 +281,7 @@ static NSArray *folderListingFields = nil; if (aName && [aName length] > 0) { aName = [aName asSafeSQLString]; - qs = [NSString stringWithFormat: @"(c_name='%@')", aName]; + qs = [NSString stringWithFormat: @"(c_name = '%@')", aName]; qualifier = [EOQualifier qualifierWithQualifierFormat: qs]; dbRecords = [[self ocsFolder] fetchFields: folderListingFields matchingQualifier: qualifier]; diff --git a/SoObjects/Mailer/SOGoMailObject.h b/SoObjects/Mailer/SOGoMailObject.h index 8089bcf0b..dee0b46f1 100644 --- a/SoObjects/Mailer/SOGoMailObject.h +++ b/SoObjects/Mailer/SOGoMailObject.h @@ -48,7 +48,7 @@ @class NGImap4Envelope; @class NGImap4EnvelopeAddress; -NSArray *SOGoMailCoreInfoKeys; +extern NSArray *SOGoMailCoreInfoKeys; @interface SOGoMailObject : SOGoMailBaseObject { diff --git a/SoObjects/SOGo/NSString+Utilities.h b/SoObjects/SOGo/NSString+Utilities.h index d6b6e278f..91c5373ca 100644 --- a/SoObjects/SOGo/NSString+Utilities.h +++ b/SoObjects/SOGo/NSString+Utilities.h @@ -1,6 +1,6 @@ /* NSString+Utilities.h - this file is part of SOGo * - * Copyright (C) 2006-2015 Inverse inc. + * Copyright (C) 2006-2022 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -51,6 +51,7 @@ /* SQL safety */ - (NSString *) asSafeSQLString; +- (NSString *) asSafeSQLLikeString; /* Unicode safety */ - (NSString *) safeString; diff --git a/SoObjects/SOGo/NSString+Utilities.m b/SoObjects/SOGo/NSString+Utilities.m index adcb67b7c..4229c6bdd 100644 --- a/SoObjects/SOGo/NSString+Utilities.m +++ b/SoObjects/SOGo/NSString+Utilities.m @@ -1,6 +1,6 @@ /* NSString+Utilities.m - this file is part of SOGo * - * Copyright (C) 2006-2015 Inverse inc. + * Copyright (C) 2006-2022 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -688,9 +688,13 @@ static int cssEscapingCount; - (NSString *) asSafeSQLString { - return [[[self stringByReplacingString: @"\\" withString: @"\\\\"] - stringByReplacingString: @"'" withString: @"\\'"] - stringByReplacingString: @"\%" withString: @"\\%"]; + return [[self stringByReplacingString: @"\\" withString: @"\\\\"] + stringByReplacingString: @"'" withString: @"\\'"]; +} + +- (NSString *) asSafeSQLLikeString +{ + return [[self asSafeSQLString] stringByReplacingString: @"\%" withString: @"\\%"]; } - (NSUInteger) countOccurrencesOfString: (NSString *) substring diff --git a/SoObjects/SOGo/SOGoGCSFolder.m b/SoObjects/SOGo/SOGoGCSFolder.m index 3c6c44606..c9ce67aae 100644 --- a/SoObjects/SOGo/SOGoGCSFolder.m +++ b/SoObjects/SOGo/SOGoGCSFolder.m @@ -1,7 +1,7 @@ /* SOGoGCSFolder.m - this file is part of SOGo * * Copyright (C) 2004-2005 SKYRIX Software AG - * Copyright (C) 2006-2014 Inverse inc. + * Copyright (C) 2006-2022 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1369,42 +1369,19 @@ static NSArray *childRecordFields = nil; withToken: (int) syncToken andBaseURL: (NSString *) baseURL { - static NSString *status[] = { @"HTTP/1.1 404 Not Found", - @"HTTP/1.1 201 Created", - @"HTTP/1.1 200 OK" }; NSMutableArray *children; NSString *href; - unsigned int statusIndex; children = [NSMutableArray arrayWithCapacity: 3]; - href = [NSString stringWithFormat: @"%@%@", - baseURL, [record objectForKey: @"c_name"]]; - [children addObject: davElementWithContent (@"href", XMLNS_WEBDAV, - href)]; - if (syncToken) - { - if ([[record objectForKey: @"c_deleted"] intValue] > 0) - statusIndex = 0; - else - { - if ([[record objectForKey: @"c_creationdate"] intValue] - >= syncToken) - statusIndex = 1; - else - statusIndex = 2; - } - } - else - statusIndex = 1; + href = [NSString stringWithFormat: @"%@%@", baseURL, [record objectForKey: @"c_name"]]; + [children addObject: davElementWithContent (@"href", XMLNS_WEBDAV, href)]; - // NSLog (@"webdav sync: %@ (%@)", href, status[statusIndex]); - [children addObject: davElementWithContent (@"status", XMLNS_WEBDAV, - status[statusIndex])]; - if (statusIndex) - [children - addObjectsFromArray: [self _davPropstatsWithProperties: properties - andMethodSelectors: selectors - fromRecord: record]]; + if ([[record objectForKey: @"c_deleted"] intValue] > 0) + [children addObject: davElementWithContent (@"status", XMLNS_WEBDAV, @"HTTP/1.1 404 Not Found")]; + else + [children addObjectsFromArray: [self _davPropstatsWithProperties: properties + andMethodSelectors: selectors + fromRecord: record]]; return davElementWithContent (@"response", XMLNS_WEBDAV, children); } @@ -1513,6 +1490,10 @@ static NSArray *childRecordFields = nil; return valid; } +/** + DAV:sync-collection Report + https://datatracker.ietf.org/doc/html/rfc6578#section-3.2 +*/ - (WOResponse *) davSyncCollection: (WOContext *) localContext { WOResponse *r; @@ -1948,8 +1929,8 @@ static NSArray *childRecordFields = nil; if (sqlFilter) { filterString = [NSMutableString stringWithCapacity: 8192]; - [filterString appendFormat: @"(c_name='%@')", - [cNames componentsJoinedByString: @"' OR c_name='"]]; + [filterString appendFormat: @"(c_name = '%@')", + [cNames componentsJoinedByString: @"' OR c_name = '"]]; if ([sqlFilter length] > 0) [filterString appendFormat: @" AND (%@)", sqlFilter]; qualifier = [EOQualifier qualifierWithQualifierFormat: filterString]; @@ -1989,10 +1970,9 @@ static NSArray *childRecordFields = nil; components = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { - currentName = [cNames objectAtIndex: count]; + currentName = [[cNames objectAtIndex: count] asSafeSQLString]; queryNameLength = idQueryOverhead + [currentName length]; - if ((currentSize + queryNameLength) - > maxQuerySize) + if ((currentSize + queryNameLength) > maxQuerySize) { records = [self _fetchComponentsWithNames: currentNames fields: fields]; [components addObjectsFromArray: records]; @@ -2003,8 +1983,11 @@ static NSArray *childRecordFields = nil; currentSize += queryNameLength; } - records = [self _fetchComponentsWithNames: currentNames fields: fields]; - [components addObjectsFromArray: records]; + if ([currentNames count]) + { + records = [self _fetchComponentsWithNames: currentNames fields: fields]; + [components addObjectsFromArray: records]; + } // NSLog (@"/fetching components matching names"); @@ -2304,6 +2287,10 @@ static NSArray *childRecordFields = nil; NSZoneFree (NULL, propertiesArray); } +/** + CALDAV:calendar-multiget REPORT + https://datatracker.ietf.org/doc/html/rfc4791#section-7.9 + */ - (WOResponse *) performMultigetInContext: (WOContext *) queryContext inNamespace: (NSString *) namespace { diff --git a/SoObjects/SOGo/SOGoWebAuthenticator.m b/SoObjects/SOGo/SOGoWebAuthenticator.m index bd386d035..9cd0e4f4c 100644 --- a/SoObjects/SOGo/SOGoWebAuthenticator.m +++ b/SoObjects/SOGo/SOGoWebAuthenticator.m @@ -1,6 +1,6 @@ /* SOGoWebAuthenticator.m - this file is part of SOGo * - * Copyright (C) 2007-2014 Inverse inc. + * Copyright (C) 2007-2022 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -390,6 +390,7 @@ { WOCookie *authCookie; NSString *cookieValue, *cookieString, *appName, *sessionKey, *userKey, *securedPassword; + BOOL isSecure; // // We create a new cookie - thus we create a new session @@ -416,8 +417,14 @@ userKey, sessionKey]; cookieValue = [NSString stringWithFormat: @"basic %@", [cookieString stringByEncodingBase64]]; + isSecure = [[[context serverURL] scheme] isEqualToString: @"https"]; authCookie = [WOCookie cookieWithName: [self cookieNameInContext: context] - value: cookieValue]; + value: cookieValue + path: nil + domain: nil + expires: nil + isSecure: isSecure + httpOnly: YES]; appName = [[context request] applicationName]; [authCookie setPath: [NSString stringWithFormat: @"/%@/", appName]]; diff --git a/SoObjects/SOGo/SQLSource.m b/SoObjects/SOGo/SQLSource.m index 3ee912ff7..9fed8b45b 100644 --- a/SoObjects/SOGo/SQLSource.m +++ b/SoObjects/SOGo/SQLSource.m @@ -776,7 +776,7 @@ if (channel) { lowerFilter = [filter lowercaseString]; - lowerFilter = [lowerFilter stringByReplacingString: @"'" withString: @"''"]; + lowerFilter = [lowerFilter asSafeSQLLikeString]; sql = [NSMutableString stringWithFormat: (@"SELECT *" @" FROM %@" diff --git a/Tests/Integration/all.py b/Tests/Integration/all.py deleted file mode 100755 index cb818980b..000000000 --- a/Tests/Integration/all.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/python - -import os, sys, unittest, getopt, traceback, time -import preferences -import sogotests -import unittest - -if __name__ == "__main__": - unittest._TextTestResult.oldStartTest = unittest._TextTestResult.startTest - unittest._TextTestResult.startTest = sogotests.UnitTestTextTestResultNewStartTest - unittest._TextTestResult.stopTest = sogotests.UnitTestTextTestResultNewStopTest - - loader = unittest.TestLoader() - modules = [] - - languages = preferences.SOGoSupportedLanguages - - # We can disable testing all languages - testLanguages = False - opts, args = getopt.getopt (sys.argv[1:], [], ["enable-languages"]) - for o, a in opts: - if o == "--enable-languages": - testLanguages = True - - - for mod in os.listdir("."): - if mod.startswith("test-") and mod.endswith(".py"): - modules.append(mod[:-3]) - __import__(mod[:-3]) - - if len(modules) > 0: - suite = loader.loadTestsFromNames(modules) - print "%d tests in modules: '%s'" % (suite.countTestCases(), - "', '".join(modules)) - runner = unittest.TextTestRunner(verbosity=2) - - if testLanguages: - prefs = preferences.preferences() - # Get the current language - userLanguageString = prefs.get ("SOGoLanguage") - if userLanguageString: - userLanguage = languages.index (userLanguageString) - else: - userLanguage = languages.index ("English") - - for i in range (0, len (languages)): - try: - prefs.set ("SOGoLanguage", i) - except Exception, inst: - print '-' * 60 - traceback.print_exc () - print '-' * 60 - - print "Running test in %s (%d/%d)" % \ - (languages[i], i + 1, len (languages)) - runner.verbosity = 2 - runner.run(suite) - # Revert to the original language - prefs.set ("SOGoLanguage", userLanguage) - else: - runner.run(suite) - - else: - print "No test available." diff --git a/Tests/Integration/config.py.in b/Tests/Integration/config.py.in deleted file mode 100644 index 7e5c1ca78..000000000 --- a/Tests/Integration/config.py.in +++ /dev/null @@ -1,38 +0,0 @@ -# setup: 4 user are needed: username, superuser, attendee1, attendee1_delegate -# superuser must be a sogo superuser... - -hostname = "localhost" -port = "80" -username = "myuser" -password = "mypass" - -superuser = "super" -superuser_password="pass" - -subscriber_username = "otheruser" -subscriber_password = "otherpass" - -attendee1 = "user@domain.com" -attendee1_username = "user" -attendee1_password = "pass" - -attendee1_delegate = "user2@domain.com" -attendee1_delegate_username = "sogo2" -attendee1_delegate_password = "sogo" - -resource_no_overbook = "res" -resource_can_overbook = "res-nolimit" - -white_listed_attendee = '{"sogo1":"John Doe "}' - -mailserver = "imaphost" - -testput_nbrdays = 30 - -sieve_server = "localhost" -sieve_port = 2000 - -sogo_user = "sogo" -sogo_tool_path = "/usr/local/sbin/sogo-tool" - -webCalendarURL = "http://inverse.ca/sogo-integration-tests/CanadaHolidays.ics" diff --git a/Tests/Integration/ev_generator.py b/Tests/Integration/ev_generator.py deleted file mode 100644 index c12dadb24..000000000 --- a/Tests/Integration/ev_generator.py +++ /dev/null @@ -1,92 +0,0 @@ -import time - -def hours(nbr): - return nbr * 3600 - -def days(nbr): - return nbr * hours(24) - -class ev_generator: - ev_templ = """ -BEGIN:VCALENDAR\r -VERSION:2.0\r -PRODID:-//Inverse//Event Generator//EN\r -CALSCALE:GREGORIAN\r -BEGIN:VTIMEZONE\r -TZID:America/Montreal\r -BEGIN:DAYLIGHT\r -TZOFFSETFROM:-0500\r -TZOFFSETTO:-0400\r -DTSTART:20070311T020000\r -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r -TZNAME:EDT\r -END:DAYLIGHT\r -BEGIN:STANDARD\r -TZOFFSETFROM:-0400\r -TZOFFSETTO:-0500\r -DTSTART:20071104T020000\r -RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r -TZNAME:EST\r -END:STANDARD\r -END:VTIMEZONE\r -BEGIN:VEVENT\r -SEQUENCE:4\r -TRANSP:OPAQUE\r -UID:%(uid)s\r -SUMMARY:%(summary)s\r -DTSTART;TZID=America/Montreal:%(start)s\r -DTEND;TZID=America/Montreal:%(end)s\r -CREATED:20080711T231608Z\r -DTSTAMP:20080711T231640Z\r -END:VEVENT\r -END:VCALENDAR\r -""" - def __init__(self, maxDays): - self.reset(maxDays) - - def reset(self, maxDays): - self.maxDays = maxDays - self.currentDay = 0 - self.currentStart = 0 - today = time.mktime(time.localtime()) - self.firstDay = today - days(maxDays + 30) - - def _calendarDate(self, eventTime): - timeStruct = time.localtime(eventTime) - return time.strftime("%Y%m%dT%H0000", timeStruct) - - def _iterValues(self): - event = None - - if (self.currentDay < self.maxDays): - eventStart = (self.firstDay - + days(self.currentDay) - + hours(self.currentStart + 8)) - eventEnd = eventStart + hours(1) - - thatDay = time.localtime(int(eventStart)) - uid = "Event%d%d" % (eventStart, eventEnd) - summary = "%s - event %d" % (time.strftime("%Y-%m-%d", thatDay), - self.currentStart) - start = self._calendarDate(eventStart) - end = self._calendarDate(eventEnd) - event = {'uid': uid, - 'summary': summary, - 'start': start, - 'end': end} - - self.currentStart = self.currentStart + 1 - if (self.currentStart > 7): - self.currentStart = 0 - self.currentDay = self.currentDay + 1 - - return event - - def iter(self): - hasMore = False - entryValues = self._iterValues() - if (entryValues is not None): - self.event = (self.ev_templ % entryValues).strip() - hasMore = True - - return hasMore diff --git a/Tests/Integration/managesieve.py b/Tests/Integration/managesieve.py deleted file mode 100644 index b248b13c5..000000000 --- a/Tests/Integration/managesieve.py +++ /dev/null @@ -1,631 +0,0 @@ -"""Sieve management client. - -A Protocol for Remotely Managing Sieve Scripts -Based on -""" - -__version__ = "0.4.2" -__author__ = """Hartmut Goebel -Ulrich Eck April 2001 -""" - -import binascii, re, socket, time, random, sys -try: - import ssl - ssl_wrap_socket = ssl.wrap_socket -except ImportError: - ssl_wrap_socket = socket.ssl - -__all__ = [ 'MANAGESIEVE', 'SIEVE_PORT', 'OK', 'NO', 'BYE', 'Debug'] - -Debug = 0 -CRLF = '\r\n' -SIEVE_PORT = 2000 - -OK = 'OK' -NO = 'NO' -BYE = 'BYE' - -AUTH_PLAIN = "PLAIN" -AUTH_LOGIN = "LOGIN" -# authentication mechanisms currently supported -# in order of preference -AUTHMECHS = [AUTH_PLAIN, AUTH_LOGIN] - -# todo: return results or raise exceptions? -# todo: on result 'BYE' quit immediatly -# todo: raise exception on 'BYE'? - -# Commands -commands = { - # name valid states - 'STARTTLS': ('NONAUTH',), - 'AUTHENTICATE': ('NONAUTH',), - 'LOGOUT': ('NONAUTH', 'AUTH', 'LOGOUT'), - 'CAPABILITY': ('NONAUTH', 'AUTH'), - 'GETSCRIPT': ('AUTH', ), - 'PUTSCRIPT': ('AUTH', ), - 'SETACTIVE': ('AUTH', ), - 'DELETESCRIPT': ('AUTH', ), - 'LISTSCRIPTS': ('AUTH', ), - 'HAVESPACE': ('AUTH', ), - # bogus command to receive a NO after STARTTLS (see starttls() ) - 'BOGUS': ('NONAUTH', 'AUTH', 'LOGOUT'), - } - -### needed -Oknobye = re.compile(r'(?P(OK|NO|BYE))' - r'( \((?P.*)\))?' - r'( (?P.*))?') -# draft-martin-managesieve-04.txt defines the size tag of literals to -# contain a '+' (plus sign) behind the digits, but timsieved does not -# send one. Thus we are less strikt here: -Literal = re.compile(r'.*{(?P\d+)\+?}$') -re_dquote = re.compile(r'"(([^"\\]|\\.)*)"') -re_esc_quote = re.compile(r'\\([\\"])') - - -class SSLFakeSocket: - """A fake socket object that really wraps a SSLObject. - - It only supports what is needed in managesieve. - """ - def __init__(self, realsock, sslobj): - self.realsock = realsock - self.sslobj = sslobj - - def send(self, str): - self.sslobj.write(str) - return len(str) - - sendall = send - - def close(self): - self.realsock.close() - -class SSLFakeFile: - """A fake file like object that really wraps a SSLObject. - - It only supports what is needed in managesieve. - """ - def __init__(self, sslobj): - self.sslobj = sslobj - - def readline(self): - str = "" - chr = None - while chr != "\n": - chr = self.sslobj.read(1) - str += chr - return str - - def read(self, size=0): - if size == 0: - return '' - else: - return self.sslobj.read(size) - - def close(self): - pass - - -def sieve_name(name): - # todo: correct quoting - return '"%s"' % name - -def sieve_string(string): - return '{%d+}%s%s' % ( len(string), CRLF, string ) - - -class MANAGESIEVE: - """Sieve client class. - - Instantiate with: MANAGESIEVE(host [, port]) - - host - host's name (default: localhost) - port - port number (default: standard Sieve port). - - use_tls - switch to TLS automatically, if server supports - keyfile - keyfile to use for TLS (optional) - certfile - certfile to use for TLS (optional) - - All Sieve commands are supported by methods of the same - name (in lower-case). - - Each command returns a tuple: (type, [data, ...]) where 'type' - is usually 'OK' or 'NO', and 'data' is either the text from the - tagged response, or untagged results from command. - - All arguments to commands are converted to strings, except for - AUTHENTICATE. - """ - - """ - However, the 'password' argument to the LOGIN command is always - quoted. If you want to avoid having an argument string quoted (eg: - the 'flags' argument to STORE) then enclose the string in - parentheses (eg: "(\Deleted)"). - - Errors raise the exception class .error(""). - IMAP4 server errors raise .abort(""), - which is a sub-class of 'error'. Mailbox status changes - from READ-WRITE to READ-ONLY raise the exception class - .readonly(""), which is a sub-class of 'abort'. - - "error" exceptions imply a program error. - "abort" exceptions imply the connection should be reset, and - the command re-tried. - "readonly" exceptions imply the command should be re-tried. - - Note: to use this module, you must read the RFCs pertaining - to the IMAP4 protocol, as the semantics of the arguments to - each IMAP4 command are left to the invoker, not to mention - the results. - """ - - class error(Exception): """Logical errors - debug required""" - class abort(error): """Service errors - close and retry""" - - def __clear_knowledge(self): - """clear/init any knowledge obtained from the server""" - self.capabilities = [] - self.loginmechs = [] - self.implementation = '' - self.supports_tls = 0 - - def __init__(self, host='', port=SIEVE_PORT, - use_tls=False, keyfile=None, certfile=None): - self.host = host - self.port = port - self.debug = Debug - self.state = 'NONAUTH' - - self.response_text = self.response_code = None - self.__clear_knowledge() - - # Open socket to server. - self._open(host, port) - - if __debug__: - self._cmd_log_len = 10 - self._cmd_log_idx = 0 - self._cmd_log = {} # Last `_cmd_log_len' interactions - if self.debug >= 1: - self._mesg('managesieve version %s' % __version__) - - # Get server welcome message, - # request and store CAPABILITY response. - typ, data = self._get_response() - if typ == 'OK': - self._parse_capabilities(data) - if use_tls and self.supports_tls: - typ, data = self.starttls(keyfile=keyfile, certfile=certfile) - if typ == 'OK': - self._parse_capabilities(data) - - - def _parse_capabilities(self, lines): - for line in lines: - if len(line) == 2: - typ, data = line - else: - assert len(line) == 1, 'Bad Capabilities line: %r' % line - typ = line[0] - data = None - if __debug__: - if self.debug >= 3: - self._mesg('%s: %r' % (typ, data)) - if typ == "IMPLEMENTATION": - self.implementation = data - elif typ == "SASL": - self.loginmechs = data.split() - elif typ == "SIEVE": - self.capabilities = data.split() - elif typ == "STARTTLS": - self.supports_tls = 1 - else: - # A client implementation MUST ignore any other - # capabilities given that it does not understand. - pass - return - - - def __getattr__(self, attr): - # Allow UPPERCASE variants of MANAGESIEVE command methods. - if commands.has_key(attr): - return getattr(self, attr.lower()) - raise AttributeError("Unknown MANAGESIEVE command: '%s'" % attr) - - - #### Private methods ### - def _open(self, host, port): - """Setup 'self.sock' and 'self.file'.""" - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.connect((self.host, self.port)) - self.file = self.sock.makefile('r') - - def _close(self): - self.file.close() - self.sock.close() - - def _read(self, size): - """Read 'size' bytes from remote.""" - data = "" - while len(data) < size: - data += self.file.read(size - len(data)) - return data - - def _readline(self): - """Read line from remote.""" - return self.file.readline() - - def _send(self, data): - return self.sock.send(data) - - def _get_line(self): - line = self._readline() - if not line: - raise self.abort('socket error: EOF') - # Protocol mandates all lines terminated by CRLF - line = line[:-2] - if __debug__: - if self.debug >= 4: - self._mesg('< %s' % line) - else: - self._log('< %s' % line) - return line - - def _simple_command(self, *args): - """Execute a command which does only return status. - - Returns (typ) with - typ = response type - - The responce code and text may be found in .response_code - and .response_text, respectivly. - """ - return self._command(*args)[0] # only return typ, ignore data - - - def _command(self, name, arg1=None, arg2=None, *options): - """ - Returns (typ, data) with - typ = response type - data = list of lists of strings read (only meaningfull if OK) - - The responce code and text may be found in .response_code - and .response_text, respectivly. - """ - if self.state not in commands[name]: - raise self.error( - 'Command %s illegal in state %s' % (name, self.state)) - # concatinate command and arguments (if any) - data = " ".join(filter(None, (name, arg1, arg2))) - if __debug__: - if self.debug >= 4: self._mesg('> %r' % data) - else: self._log('> %s' % data) - try: - try: - self._send('%s%s' % (data, CRLF)) - for o in options: - if __debug__: - if self.debug >= 4: self._mesg('> %r' % o) - else: self._log('> %r' % data) - self._send('%s%s' % (o, CRLF)) - except (socket.error, OSError), val: - raise self.abort('socket error: %s' % val) - return self._get_response() - except self.abort, val: - if __debug__: - if self.debug >= 1: - self.print_log() - raise - - - def _readstring(self, data): - if data[0] == ' ': # space -> error - raise self.error('Unexpected space: %r' % data) - elif data[0] == '"': # handle double quote: - if not self._match(re_dquote, data): - raise self.error('Unmatched quote: %r' % data) - snippet = self.mo.group(1) - return re_esc_quote.sub(r'\1', snippet), data[self.mo.end():] - elif self._match(Literal, data): - # read a 'literal' string - size = int(self.mo.group('size')) - if __debug__: - if self.debug >= 4: - self._mesg('read literal size %s' % size) - return self._read(size), self._get_line() - else: - data = data.split(' ', 1) - if len(data) == 1: - data.append('') - return data - - def _get_response(self): - """ - Returns (typ, data) with - typ = response type - data = list of lists of strings read (only meaningfull if OK) - - The responce code and text may be found in .response_code - and .response_text, respectivly. - """ - - """ - response-deletescript = response-oknobye - response-authenticate = *(string CRLF) (response-oknobye) - response-capability = *(string [SP string] CRLF) response-oknobye - response-listscripts = *(string [SP "ACTIVE"] CRLF) response-oknobye - response-oknobye = ("OK" / "NO" / "BYE") [SP "(" resp-code ")"] [SP string] CRLF - string = quoted / literal - quoted = <"> *QUOTED-CHAR <"> - literal = "{" number "+}" CRLF *OCTET - ;; The number represents the number of octets - ;; MUST be literal-utf8 except for values - ---> a response either starts with a quote-charakter, a left-bracket or - OK, NO, BYE - -"quoted" CRLF -"quoted" SP "quoted" CRLF -{size} CRLF *OCTETS CRLF -{size} CRLF *OCTETS CRLF -[A-Z-]+ CRLF - - """ - data = [] ; dat = None - resp = self._get_line() - while 1: - if self._match(Oknobye, resp): - typ, code, dat = self.mo.group('type','code','data') - if __debug__: - if self.debug >= 1: - self._mesg('%s response: %s %s' % (typ, code, dat)) - self.response_code = code - self.response_text = None - if dat: - self.response_text = self._readstring(dat)[0] - - # if server quits here, send code instead of empty data - if typ == "BYE": - return typ, code - - return typ, data -## elif 0: -## dat2 = None -## dat, resp = self._readstring(resp) -## if resp.startswith(' '): -## dat2, resp = self._readstring(resp[1:]) -## data.append( (dat, dat2)) -## resp = self._get_line() - else: - dat = [] - while 1: - dat1, resp = self._readstring(resp) - if __debug__: - if self.debug >= 4: - self._mesg('read: %r' % (dat1,)) - if self.debug >= 5: - self._mesg('rest: %r' % (resp,)) - dat.append(dat1) - if not resp.startswith(' '): - break - resp = resp[1:] - if len(dat) == 1: - dat.append(None) - data.append(dat) - resp = self._get_line() - return self.error('Should not come here') - - - def _match(self, cre, s): - # Run compiled regular expression match method on 's'. - # Save result, return success. - self.mo = cre.match(s) - if __debug__: - if self.mo is not None and self.debug >= 5: - self._mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)) - return self.mo is not None - - - if __debug__: - - def _mesg(self, s, secs=None): - if secs is None: - secs = time.time() - tm = time.strftime('%M:%S', time.localtime(secs)) - sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s)) - sys.stderr.flush() - - def _log(self, line): - # Keep log of last `_cmd_log_len' interactions for debugging. - self._cmd_log[self._cmd_log_idx] = (line, time.time()) - self._cmd_log_idx += 1 - if self._cmd_log_idx >= self._cmd_log_len: - self._cmd_log_idx = 0 - - def print_log(self): - self.self._mesg('last %d SIEVE interactions:' % len(self._cmd_log)) - i, n = self._cmd_log_idx, self._cmd_log_len - while n: - try: - self.self._mesg(*self._cmd_log[i]) - except: - pass - i += 1 - if i >= self._cmd_log_len: - i = 0 - n -= 1 - - ### Public methods ### - def authenticate(self, mechanism, *authobjects): - """Authenticate command - requires response processing.""" - # command-authenticate = "AUTHENTICATE" SP auth-type [SP string] *(CRLF string) - # response-authenticate = *(string CRLF) (response-oknobye) - mech = mechanism.upper() - if not mech in self.loginmechs: - raise self.error("Server doesn't allow %s authentication." % mech) - - if mech == AUTH_LOGIN: - authobjects = [ sieve_name(binascii.b2a_base64(ao)[:-1]) - for ao in authobjects - ] - elif mech == AUTH_PLAIN: - if len(authobjects) < 3: - # assume authorization identity (authzid) is missing - # and these two authobjects are username and password - authobjects.insert(0, '') - ao = '\0'.join(authobjects) - ao = binascii.b2a_base64(ao)[:-1] - authobjects = [ sieve_string(ao) ] - else: - raise self.error("managesieve doesn't support %s authentication." % mech) - - typ, data = self._command('AUTHENTICATE', - sieve_name(mech), *authobjects) - if typ == 'OK': - self.state = 'AUTH' - return typ - - - def login(self, auth, user, password): - """ - Authenticate to the Sieve server using the best mechanism available. - """ - for authmech in AUTHMECHS: - if authmech in self.loginmechs: - authobjs = [auth, user, password] - if authmech == AUTH_LOGIN: - authobjs = [user, password] - return self.authenticate(authmech, *authobjs) - else: - raise self.abort('No matching authentication mechanism found.') - - def logout(self): - """Terminate connection to server.""" - # command-logout = "LOGOUT" CRLF - # response-logout = response-oknobye - typ = self._simple_command('LOGOUT') - self.state = 'LOGOUT' - self._close() - return typ - - - def listscripts(self): - """Get a list of scripts on the server. - - (typ, [data]) = .listscripts() - - if 'typ' is 'OK', 'data' is list of (scriptname, active) tuples. - """ - # command-listscripts = "LISTSCRIPTS" CRLF - # response-listscripts = *(sieve-name [SP "ACTIVE"] CRLF) response-oknobye - typ, data = self._command('LISTSCRIPTS') - if typ != 'OK': return typ, data - scripts = [] - for dat in data: - if __debug__: - if not len(dat) in (1, 2): - self.error("Unexpected result from LISTSCRIPTS: %r" (dat,)) - scripts.append( (dat[0], dat[1] is not None )) - return typ, scripts - - - def getscript(self, scriptname): - """Get a script from the server. - - (typ, scriptdata) = .getscript(scriptname) - - 'scriptdata' is the script data. - """ - # command-getscript = "GETSCRIPT" SP sieve-name CRLF - # response-getscript = [string CRLF] response-oknobye - - typ, data = self._command('GETSCRIPT', sieve_name(scriptname)) - if typ != 'OK': return typ, data - if len(data) != 1: - self.error('GETSCRIPT returned more than one string/script') - # todo: decode data? - return typ, data[0][0] - - - def putscript(self, scriptname, scriptdata): - """Put a script onto the server.""" - # command-putscript = "PUTSCRIPT" SP sieve-name SP string CRLF - # response-putscript = response-oknobye - return self._simple_command('PUTSCRIPT', - sieve_name(scriptname), - sieve_string(scriptdata) - ) - - def deletescript(self, scriptname): - """Delete a scripts at the server.""" - # command-deletescript = "DELETESCRIPT" SP sieve-name CRLF - # response-deletescript = response-oknobye - return self._simple_command('DELETESCRIPT', sieve_name(scriptname)) - - - def setactive(self, scriptname): - """Mark a script as the 'active' one.""" - # command-setactive = "SETACTIVE" SP sieve-name CRLF - # response-setactive = response-oknobye - return self._simple_command('SETACTIVE', sieve_name(scriptname)) - - - def havespace(self, scriptname, size): - # command-havespace = "HAVESPACE" SP sieve-name SP number CRLF - # response-havespace = response-oknobye - return self._simple_command('HAVESPACE', - sieve_name(scriptname), - str(size)) - - - def capability(self): - """ - Isse a CAPABILITY command and return the result. - - As a side-effect, on succes these attributes are (re)set: - self.implementation - self.loginmechs - self.capabilities - self.supports_tls - """ - # command-capability = "CAPABILITY" CRLF - # response-capability = *(string [SP string] CRLF) response-oknobye - typ, data = self._command('CAPABILITY') - if typ == 'OK': - self._parse_capabilities(data) - return typ, data - - - def starttls(self, keyfile=None, certfile=None): - """Puts the connection to the SIEVE server into TLS mode. - - If the server supports TLS, this will encrypt the rest of the SIEVE - session. If you provide the keyfile and certfile parameters, - the identity of the SIEVE server and client can be checked. This, - however, depends on whether the socket module really checks the - certificates. - """ - # command-starttls = "STARTTLS" CRLF - # response-starttls = response-oknobye - typ, data = self._command('STARTTLS') - if typ == 'OK': - sslobj = ssl_wrap_socket(self.sock, keyfile, certfile) - self.sock = SSLFakeSocket(self.sock, sslobj) - self.file = SSLFakeFile(sslobj) - # MUST discard knowledge obtained from the server - self.__clear_knowledge() - # Some servers send capabilities after TLS handshake, some - # do not. We send a bogus command, and expect a NO. If you - # get something else instead, read the extra NO to clear - # the buffer. - typ, data = self._command('BOGUS') - if typ != 'NO': - typ, data = self._get_response() - # server may not advertise capabilities, thus we need to ask - self.capability() - if self.debug >= 3: self._mesg('started Transport Layer Security (TLS)') - return typ, data diff --git a/Tests/Integration/preferences.py b/Tests/Integration/preferences.py deleted file mode 100644 index 2e47cce87..000000000 --- a/Tests/Integration/preferences.py +++ /dev/null @@ -1,139 +0,0 @@ -from config import hostname, port, username, password -import webdavlib -import urllib -import base64 -import simplejson - -import sogoLogin - - - -# must be kept in sync with SoObjects/SOGo/SOGoDefaults.plist -# this should probably be fetched magically... -SOGoSupportedLanguages = [ "Arabic", "Basque", "Catalan", "ChineseChina", "ChineseTaiwan", "Croatian", "Czech", "Dutch", "Danish", "Welsh", "English", "Finnish", - "SpanishSpain", "SpanishArgentina", "French", "German", "Hebrew", - "Icelandic", "Italian", "Latvian", "Lithuanian", "Macedonian", "Hungarian", "Portuguese", "BrazilianPortuguese", - "NorwegianBokmal", "NorwegianNynorsk", "Polish", "Russian", "Serbian", "Slovak", - "Slovenian", "Swedish", "TurkishTurkey", "Ukrainian" ]; -daysBetweenResponseList=[1,2,3,5,7,14,21,30] - -class HTTPPreferencesPOST (webdavlib.HTTPPOST): - cookie = None - - def prepare_headers (self): - headers = webdavlib.HTTPPOST.prepare_headers(self) - if self.cookie: - headers["Cookie"] = self.cookie - return headers - -class HTTPPreferencesGET (webdavlib.HTTPGET): - cookie = None - - def prepare_headers (self): - headers = webdavlib.HTTPGET.prepare_headers(self) - if self.cookie: - headers["Cookie"] = self.cookie - return headers - -class preferences: - login = username - passw = password - - def __init__(self, otherLogin = None, otherPassword = None): - if otherLogin and otherPassword: - self.login = otherLogin - self.passw = otherPassword - - self.client = webdavlib.WebDAVClient(hostname, port) - - authCookie = sogoLogin.getAuthCookie(hostname, port, self.login, self.passw) - self.cookie = authCookie - - # map between preferences/jsonDefaults and the webUI names - # should probably be unified... - self.preferencesMap = { - "SOGoLanguage": "language", - "SOGoTimeZone": "timezone", - "SOGoSieveFilters": "sieveFilters", - - # Vacation stuff - "Vacation": "enableVacation", # to disable, don't specify it - "autoReplyText": "autoReplyText", # string - "autoReplyEmailAddresses": "autoReplyEmailAddresses", # LIST - "daysBetweenResponse": "daysBetweenResponsesList", - "ignoreLists": "ignoreLists", #bool - - # forward stuff - "Forward": "enableForward", # to disable, don't specify it - "forwardAddress": "forwardAddress", - "keepCopy": "forwardKeepCopy", - - # Calendar stuff - "enablePreventInvitations": "preventInvitations", - "PreventInvitations": "PreventInvitations", - "whiteList": "whiteList", - } - - def set(self, preference, value=None): - # if preference is a dict, set all prefs found in the dict - content="" - if isinstance(preference, dict): - for k,v in preference.items(): - content+="%s=%s&" % (self.preferencesMap[k], urllib.quote(v)) - else: - # assume it is a str - formKey = self.preferencesMap[preference] - content = "%s=%s&hasChanged=1" % (formKey, urllib.quote(value)) - - - url = "/SOGo/so/%s/preferences" % self.login - - post = HTTPPreferencesPOST (url, content) - post.content_type = "application/x-www-form-urlencoded" - post.cookie = self.cookie - - self.client.execute (post) - - # Raise an exception if the pref wasn't properly set - if post.response["status"] != 200: - raise Exception ("failure setting prefs, (code = %d)" \ - % post.response["status"]) - - def get(self, preference=None): - url = "/SOGo/so/%s/preferences/jsonDefaults" % self.login - get = HTTPPreferencesGET (url) - get.cookie = self.cookie - self.client.execute (get) - content = simplejson.loads(get.response['body']) - result = None - try: - if preference: - result = content[preference] - else: - result = content - except: - pass - return result - - def get_settings(self, preference=None): - url = "/SOGo/so/%s/preferences/jsonSettings" % self.login - get = HTTPPreferencesGET (url) - get.cookie = self.cookie - self.client.execute (get) - content = simplejson.loads(get.response['body']) - result = None - try: - if preference: - result = content[preference] - else: - result = content - except: - pass - return result - -# Simple main to test this class -if __name__ == "__main__": - p = preferences () - print p.get ("SOGoLanguage") - p.set ("SOGoLanguage", SOGoSupportedLanguages.index("French")) - print p.get ("SOGoLanguage") diff --git a/Tests/Integration/propfind.py b/Tests/Integration/propfind.py deleted file mode 100755 index 117cf0409..000000000 --- a/Tests/Integration/propfind.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/python - -from config import hostname, port, username, password - -import webdavlib - -import sys -import getopt -import xml.dom.minidom - -def parseArguments(): - arguments = {} - -depth = "0" -quiet = False -(opts, args) = getopt.getopt(sys.argv[1:], "d:q", ["depth=", "quiet"]) - -for pair in opts: - if (pair[0] == "-d" or pair[0] == "--depth"): - depth = pair[1] - elif (pair[0] == "-q" or pair[0] == "--quiet"): - quiet = True - -# print "depth: " + depth - -nargs = len(args) -if (nargs > 0): - resource = args[0] - if (nargs > 1): - properties = args[1:] - else: - properties = [ "allprop" ] -else: - print "resource required" - sys.exit(-1) - -client = webdavlib.WebDAVClient(hostname, port, username, password) -propfind = webdavlib.WebDAVPROPFIND(resource, properties, depth) -client.execute(propfind) - -sys.stderr.write("response:\n\n") -print propfind.response["body"] - -if propfind.response.has_key("document"): - sys.stderr.write("document tree:\n") - elem = propfind.response["document"] - dom = xml.dom.minidom.parseString(xml.etree.ElementTree.tostring(elem)) - print dom.toprettyxml() - diff --git a/Tests/Integration/sogoLogin.py b/Tests/Integration/sogoLogin.py deleted file mode 100644 index 6eef104c9..000000000 --- a/Tests/Integration/sogoLogin.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/python - -from config import hostname, port, username, password -import sys, getopt -import webdavlib -import urllib -import urllib2 -import base64 -import simplejson -import cookielib - -def getAuthCookie(hostname, port, username, password) : - cjar = cookielib.CookieJar(); - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cjar)) - urllib2.install_opener(opener) - - creds = urllib.urlencode([("userName",username), ("password", password)]) - req = urllib2.Request("http://%s:%s/SOGo/connect" % (hostname, port), creds) - - fd = urllib2.urlopen(req) - - for cookie in cjar : - if cookie.name == "0xHIGHFLYxSOGo": - authinfo = cookie.value - break - - return "0xHIGHFLYxSOGo="+authinfo - -def usage() : - msg ="""Usage: -%s [-h] [-H | --host=hostname] [-p|--passwd=password] \ -[-P|--port=port] [-u|--user=username]\n""" % sys.argv[0] - - sys.stderr.write(msg); - -if __name__ == "__main__" : - try: - opts, args = getopt.getopt (sys.argv[1:], "hH:p:P:u:", \ - ("host=", "passwd=", "port=", "user=")); - except getopt.GetoptError: - usage() - exit(1) - for o, v in opts : - if o == "-h" : - usage() - exit(1) - elif o == "-H" or o == "--host" : - hostname = v - elif o == "-p" or o == "--passwd" : - password = v - elif o == "-P" or o == "--port" : - port = v - elif o == "-u" or o == "--user" : - username = v - - print getAuthCookie(hostname, port, username, password) diff --git a/Tests/Integration/sogotests.py b/Tests/Integration/sogotests.py deleted file mode 100644 index becf4f78c..000000000 --- a/Tests/Integration/sogotests.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys -import unittest -import time - -def UnitTestTextTestResultNewStartTest(self, test): - self.xstartTime = time.time() - self.oldStartTest(test) - -def UnitTestTextTestResultNewStopTest(self, test): - unittest.TestResult.stopTest(self, test) - endTime = time.time() - delta = endTime - self.xstartTime - print " %f ms" % delta - -def runTests(): - unittest._TextTestResult.oldStartTest = unittest._TextTestResult.startTest - unittest._TextTestResult.startTest = UnitTestTextTestResultNewStartTest - unittest._TextTestResult.stopTest = UnitTestTextTestResultNewStopTest - - argv = [] - argv.extend(sys.argv) - argv.append("-v") - unittest.main(argv=argv) diff --git a/Tests/Integration/test-caldav-scheduling.py b/Tests/Integration/test-caldav-scheduling.py deleted file mode 100755 index 56a8a046a..000000000 --- a/Tests/Integration/test-caldav-scheduling.py +++ /dev/null @@ -1,1079 +0,0 @@ -#!/usr/bin/python - -# setup: 4 users are needed: username, attendee1_username, -# attendee1_delegate_username and superuser. -# when writing new tests, avoid using superuser when not absolutely needed - -# TODO -# - Individual tests should set the ACLs themselves on Resources tests - -from config import hostname, port, username, password, \ - superuser, superuser_password, \ - attendee1, attendee1_username, \ - attendee1_password, \ - attendee1_delegate, attendee1_delegate_username, \ - attendee1_delegate_password, \ - resource_no_overbook, resource_can_overbook - -import datetime -import dateutil.tz -import sogotests -import sys -import time -import unittest -import utilities -import vobject -import vobject.base -import vobject.icalendar -import webdavlib -import StringIO -import xml.etree.ElementTree - -class CalDAVPropertiesTest(unittest.TestCase): - def setUp(self): - self.client = webdavlib.WebDAVClient(hostname, port, - username, password) - self.test_calendar \ - = "/SOGo/dav/%s/Calendar/test-dav-properties/" % username - mkcol = webdavlib.WebDAVMKCOL(self.test_calendar) - self.client.execute(mkcol) - - def tearDown(self): - delete = webdavlib.WebDAVDELETE(self.test_calendar) - self.client.execute(delete) - - def testDavScheduleCalendarTransparency(self): - """{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp""" - - ## PROPFIND - propfind = webdavlib.WebDAVPROPFIND(self.test_calendar, - ["{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp"], - 0) - self.client.execute(propfind) - response = propfind.response["document"].find('{DAV:}response') - propstat = response.find('{DAV:}propstat') - status = propstat.find('{DAV:}status').text[9:12] - - self.assertEquals(status, "200", - "schedule-calendar-transp marked as 'Not Found' in response") - transp = propstat.find('{DAV:}prop/{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp') - values = transp.getchildren() - self.assertEquals(len(values), 1, "one and only one element expected") - value = values[0] - self.assertTrue(xml.etree.ElementTree.iselement(value)) - ns = value.tag[0:31] - tag = value.tag[31:] - self.assertTrue(ns == "{urn:ietf:params:xml:ns:caldav}", - "schedule-calendar-transp must have a value in"\ - " namespace '%s', not '%s'" - % ("urn:ietf:params:xml:ns:caldav", ns)) - self.assertTrue(tag == "opaque", - "schedule-calendar-transp must be 'opaque' on new" \ - " collections, not '%s'" % tag) - - ## PROPPATCH - newValueNode = "{urn:ietf:params:xml:ns:caldav}thisvaluedoesnotexist" - proppatch = webdavlib.WebDAVPROPPATCH(self.test_calendar, - {"{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp": \ - { newValueNode: True }}) - self.client.execute(proppatch) - self.assertEquals(proppatch.response["status"], 400, - "expecting failure when setting transparency to" \ - " an invalid value") - - newValueNode = "{urn:ietf:params:xml:ns:caldav}transparent" - proppatch = webdavlib.WebDAVPROPPATCH(self.test_calendar, - {"{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp": \ - { newValueNode: True }}) - self.client.execute(proppatch) - self.assertEquals(proppatch.response["status"], 207, - "failure (%s) setting transparency to" \ - " 'transparent': '%s'" - % (proppatch.response["status"], - proppatch.response["body"])) - - newValueNode = "{urn:ietf:params:xml:ns:caldav}opaque" - proppatch = webdavlib.WebDAVPROPPATCH(self.test_calendar, - {"{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp": \ - { newValueNode: True }}) - self.client.execute(proppatch) - self.assertEquals(proppatch.response["status"], 207, - "failure (%s) setting transparency to" \ - " 'transparent': '%s'" - % (proppatch.response["status"], - proppatch.response["body"])) - -class CalDAVSchedulingTest(unittest.TestCase): - def setUp(self): - self.superuser_client = webdavlib.WebDAVClient(hostname, port, - superuser, superuser_password) - self.client = webdavlib.WebDAVClient(hostname, port, - username, password) - self.attendee1_client = webdavlib.WebDAVClient(hostname, port, - attendee1_username, attendee1_password) - self.attendee1_delegate_client = webdavlib.WebDAVClient(hostname, port, - attendee1_delegate_username, attendee1_delegate_password) - - utility = utilities.TestUtility(self, self.client) - (self.user_name, self.user_email) = utility.fetchUserInfo(username) - (self.attendee1_name, self.attendee1_email) = utility.fetchUserInfo(attendee1) - (self.attendee1_delegate_name, self.attendee1_delegate_email) = utility.fetchUserInfo(attendee1_delegate) - (self.res_no_ob_name, self.res_no_ob_email) = utility.fetchUserInfo(resource_no_overbook) - (self.res_can_ob_name, self.res_can_ob_email) = utility.fetchUserInfo(resource_can_overbook) - - self.user_calendar = "/SOGo/dav/%s/Calendar/personal/" % username - self.attendee1_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1 - self.attendee1_delegate_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1_delegate - self.res_calendar = "/SOGo/dav/%s/Calendar/personal/" % resource_no_overbook - self.res_ob_calendar = "/SOGo/dav/%s/Calendar/personal/" % resource_can_overbook - - # fetch non existing event to let sogo create the calendars in the db - self._getEvent(self.client, "%snonexistent" % self.user_calendar, exp_status=404) - self._getEvent(self.attendee1_client, "%snonexistent" % self.attendee1_calendar, exp_status=404) - self._getEvent(self.attendee1_delegate_client, "%snonexistent" % - self.attendee1_delegate_calendar, exp_status=404) - - # list of ics used by the test. - # tearDown will loop over this and wipe them in all users' calendar - self.ics_list = [] - - - def tearDown(self): - # delete all created events from all users' calendar - for ics in self.ics_list: - self._deleteEvent(self.superuser_client, - "%s%s" % (self.user_calendar, ics), None) - self._deleteEvent(self.superuser_client, - "%s%s" % (self.attendee1_calendar, ics), None) - self._deleteEvent(self.superuser_client, - "%s%s" % (self.attendee1_delegate_calendar, ics), None) - self._deleteEvent(self.superuser_client, - "%s%s" % (self.res_calendar, ics), None) - self._deleteEvent(self.superuser_client, - "%s%s" % (self.res_ob_calendar, ics), None) - - def _newEvent(self, summary="test event", uid="test", transp=0): - transparency = ("OPAQUE", "TRANSPARENT") - - newCal = vobject.iCalendar() - vevent = newCal.add('vevent') - vevent.add('summary').value = summary - vevent.add('transp').value = transparency[transp] - - now = datetime.datetime.now(dateutil.tz.gettz("America/Montreal")) - startdate = vevent.add('dtstart') - startdate.value = now - enddate = vevent.add('dtend') - enddate.value = now + datetime.timedelta(0, 3600) - vevent.add('uid').value = uid - vevent.add('dtstamp').value = now - vevent.add('last-modified').value = now - vevent.add('created').value = now - vevent.add('class').value = "PUBLIC" - vevent.add('sequence').value = "0" - - return newCal - - def _putEvent(self, client, filename, event, exp_status = 201): - put = webdavlib.HTTPPUT(filename, event.serialize()) - put.content_type = "text/calendar; charset=utf-8" - client.execute(put) - if exp_status is not None: - self.assertEquals(put.response["status"], exp_status) - - def _postEvent(self, client, outbox, event, originator, recipients, - exp_status = 200): - post = webdavlib.CalDAVPOST(outbox, event.serialize(), - originator, recipients) - client.execute(post) - if exp_status is not None: - self.assertEquals(post.response["status"], exp_status) - - def _getEvent(self, client, filename, exp_status = 200): - get = webdavlib.HTTPGET(filename) - client.execute(get) - - if exp_status is not None: - self.assertEquals(get.response["status"], exp_status) - - if get.response["headers"]["content-type"].startswith("text/calendar"): - stream = StringIO.StringIO(get.response["body"]) - event = vobject.base.readComponents(stream).next() - else: - event = None - - return event - - def _deleteEvent(self, client, filename, exp_status = 204): - delete = webdavlib.WebDAVDELETE(filename) - client.execute(delete) - if exp_status is not None: - self.assertEquals(delete.response["status"], exp_status) - - def _getAllEvents(self, client, collection, exp_status = 207): - propfind = webdavlib.WebDAVPROPFIND(collection, None) - client.execute(propfind) - if exp_status is not None: - self.assertEquals(propfind.response["status"], exp_status) - - content = [] - nodes = propfind.response["document"].findall('{DAV:}response') - for node in nodes: - responseHref = node.find('{DAV:}href').text - content += [responseHref] - - return content - - def _deleteAllEvents(self, client, collection, exp_status = 204): - content = self._getAllEvents(client, collection) - for item in content: - self._deleteEvent(client, item) - - def _eventAttendees(self, event): - attendees = {} - - event_component = event.vevent - for child in event_component.getChildren(): - if child.name == "ATTENDEE": - try: - delegated_to = child.delegated_to_param - except: - delegated_to = "(none)" - try: - delegated_from = child.delegated_from_param - except: - delegated_from = "(none)" - attendees[child.value] = ("%s/%s/%s" - % (child.partstat_param, - delegated_to, - delegated_from)) - - return attendees - - def _compareAttendees(self, compared_event, event): - compared_attendees = self._eventAttendees(compared_event) - compared_emails = compared_attendees.keys() - self.assertTrue(len(compared_emails) > 0, - "no attendee found") - compared_emails.sort() - - attendees = self._eventAttendees(event) - emails = attendees.keys() - emails.sort() - - self.assertEquals(len(compared_emails), len(emails), - "number of attendees is not equal" - + " (actual: %d, exp: %d)" - % (len(compared_emails), len(emails))) - - for email in emails: - self.assertEquals(compared_attendees[email], - attendees[email], - "partstat for attendee '%s' does not match" - " (actual: '%s', expected: '%s')" - % (email, - compared_attendees[email], attendees[email])) - - def testAddAttendee(self): - """ add attendee after event creation """ - - # make sure the event doesn't exist - ics_name = "test-add-attendee.ics" - self.ics_list += [ics_name] - - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ics_name), None) - self._deleteEvent(self.attendee1_client, - "%s%s" % (self.attendee1_calendar,ics_name), None) - - # 1. create an event in the organiser's calendar - event = self._newEvent(summary="Test add attendee", uid="Test add attendee") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # 2. add an attendee - event.add("method").value = "REQUEST" - attendee = event.vevent.add('attendee') - attendee.cn_param = self.attendee1_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.attendee1_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event, - exp_status=204) - - - # 3. verify that the attendee has the event - attendee_event = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name)) - - # 4. make sure the received event match the original one - # XXX is this enough? - self.assertEquals(event.vevent.uid, attendee_event.vevent.uid) - - def testUninviteAttendee(self): - """ Remove attendee after event creation """ - - # make sure the event doesn't exist - ics_name = "test-remove-attendee.ics" - self.ics_list += [ics_name] - - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ics_name), None) - self._deleteEvent(self.attendee1_client, - "%s%s" % (self.attendee1_calendar,ics_name), None) - - # 1. create an event in the organiser's calendar - event = self._newEvent(summary="Test uninvite attendee", uid="Test uninvite attendee") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # keep a copy around for updates without other attributes - noAttendeeEvent = vobject.iCalendar() - noAttendeeEvent.copy(event) - - # 2. add an attendee - event.add("method").value = "REQUEST" - attendee = event.vevent.add('attendee') - attendee.cn_param = self.attendee1_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.attendee1_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event, - exp_status=204) - - # 3. verify that the attendee has the event - attendee_event = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name)) - - # 4. make sure the received event match the original one - self.assertEquals(event.vevent.uid, attendee_event.vevent.uid) - - # 5. uninvite the attendee - put the event back without the attendee - now = datetime.datetime.now(dateutil.tz.gettz("America/Montreal")) - noAttendeeEvent.vevent.last_modified.value = now - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), noAttendeeEvent, - exp_status=204) - - # 6. verify that the attendee doesn't have the event anymore - attendee_event = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), 404) - - def testResourceNoOverbook(self): - """ try to overbook a resource """ - - # make sure there are no events in the resource calendar - self._deleteAllEvents(self.superuser_client, self.res_calendar) - - # make sure the event doesn't exist - ics_name = "test-no-overbook.ics" - self.ics_list += [ics_name] - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ics_name), None) - - ob_ics_name = "test-no-overbook-overlap.ics" - self.ics_list += [ob_ics_name] - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ob_ics_name), None) - - # 1. create an event in the organiser's calendar - event = self._newEvent(summary="Test no overbook", uid="test no overbook") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - attendee = event.vevent.add('attendee') - attendee.cn_param = self.res_no_ob_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.res_no_ob_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # 2. create a second event overlapping the first one - event = self._newEvent(summary="Test no overbook - overlap", uid="test no overbook - overlap") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - attendee = event.vevent.add('attendee') - attendee.cn_param = self.res_no_ob_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.res_no_ob_email - - # put the event - should trigger a 409 - self._putEvent(self.client, "%s%s" % (self.user_calendar, ob_ics_name), event, exp_status=409) - - def testResourceCanOverbook(self): - """ try to overbook a resource - multiplebookings=0""" - - # make sure there are no events in the resource calendar - self._deleteAllEvents(self.superuser_client, self.res_ob_calendar) - - # make sure the event doesn't exist - ics_name = "test-can-overbook.ics" - self.ics_list += [ics_name] - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ics_name), None) - - ob_ics_name = "test-can-overbook-overlap.ics" - self.ics_list += [ob_ics_name] - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ob_ics_name), None) - - # 1. create an event in the organiser's calendar - event = self._newEvent(summary="Test can overbook", uid="test can overbook") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - attendee = event.vevent.add('attendee') - attendee.cn_param = self.res_can_ob_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.res_can_ob_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # 2. create a second event overlapping the first one - event = self._newEvent(summary="Test can overbook - overlap", uid="test can overbook - overlap") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - attendee = event.vevent.add('attendee') - attendee.cn_param = self.res_can_ob_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.res_can_ob_email - - # put the event - should be fine since we can overbook this one - self._putEvent(self.client, "%s%s" % (self.user_calendar, ob_ics_name), event) - - def testResourceBookingOverlapDetection(self): - """ Resource booking overlap detection - bug #1837""" - - # There used to be some problems with recurring events and resources booking - # This test implements these edge cases - - # 1. Create recurring event (with resource) - # 2. Create single event overlaping one instance for the previous event - # (should fail) - # 3. Create recurring event which _doesn't_ overlap the first event - # (should be OK, used to fail pre1.3.17) - # 4. Create recurring event overlapping the previous recurring event - # (should fail) - - # make sure there are no events in the resource calendar - self._deleteAllEvents(self.superuser_client, self.res_calendar) - - # make sure the event doesn't exist - ics_name = "test-res-overlap-detection.ics" - self.ics_list += [ics_name] - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ics_name), None) - - overlap_ics_name = "test-res-overlap-detection-overlap.ics" - self.ics_list += [overlap_ics_name] - self._deleteEvent(self.client, - "%s%s" % (self.attendee1_calendar,overlap_ics_name), None) - - nooverlap_recurring_ics_name = "test-res-overlap-detection-nooverlap.ics" - self.ics_list += [nooverlap_recurring_ics_name] - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,nooverlap_recurring_ics_name), None) - - overlap_recurring_ics_name = "test-res-overlap-detection-overlap-recurring.ics" - self.ics_list += [overlap_recurring_ics_name] - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,overlap_recurring_ics_name), None) - - # 1. create recurring event with resource - event = self._newEvent(summary="recurring event with resource", - uid="recurring event w resource") - event.vevent.add('rrule').value = "FREQ=DAILY;COUNT=5" - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - attendee = event.vevent.add('attendee') - attendee.cn_param = self.res_no_ob_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.res_no_ob_email - - # keep a copy around for #3 - nooverlap_event = vobject.iCalendar() - nooverlap_event.copy(event) - - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # 2. Create single event overlaping one instance for the previous event - event = self._newEvent(summary="recurring event with resource", - uid="recurring event w resource - overlap") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.attendee1_name - organizer.value = self.attendee1_email - attendee = event.vevent.add('attendee') - attendee.cn_param = self.res_no_ob_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.res_no_ob_email - # should fail - self._putEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, overlap_ics_name), event, exp_status=409) - - # 3. Create recurring event which _doesn't_ overlap the first event - # (should be OK, used to fail pre1.3.17) - # shift the start date to one hour after the original event end time - nstartdate = nooverlap_event.vevent.dtend.value + datetime.timedelta(0, 3600) - nooverlap_event.vevent.dtstart.value = nstartdate - nooverlap_event.vevent.dtend.value = nstartdate + datetime.timedelta(0, 3600) - nooverlap_event.vevent.uid.value = "recurring - nooverlap" - - self._putEvent(self.client, "%s%s" % (self.user_calendar, nooverlap_recurring_ics_name), nooverlap_event) - - # 4. Create recurring event overlapping the previous recurring event - # should fail with a 409 - nstartdate = nooverlap_event.vevent.dtstart.value + datetime.timedelta(0, 300) - nooverlap_event.vevent.dtstart.value = nstartdate - nooverlap_event.vevent.dtend.value = nstartdate + datetime.timedelta(0, 3600) - nooverlap_event.vevent.uid.value = "recurring - overlap" - self._putEvent(self.client, "%s%s" % (self.user_calendar, overlap_recurring_ics_name), nooverlap_event, exp_status=409) - - - def testRruleExceptionInvitationDance(self): - """ RRULE exception invitation dance """ - - # This workflow is based on what lightning 1.2.1 does - # create a reccurring event - # add an exception - # invite bob to the exception: - # bob is declined in the master event - # bob needs-action in the exception - # bob accepts - # bob is declined in the master event - # bob is accepted in the exception - # the organizer 'uninvites' bob - # the event disappears from bob's calendar - # bob isn't in the master+exception event - - ics_name = "test-rrule-exception-invitation-dance.ics" - self.ics_list += [ics_name] - - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar, ics_name), None) - self._deleteEvent(self.attendee1_client, - "%s%s" % (self.attendee1_calendar, ics_name), None) - - # 1. create a recurring event in the organiser's calendar - summary="Test reccuring exception invite cancel" - uid="Test-recurring-exception-invite-cancel" - event = self._newEvent(summary, uid) - event.vevent.add('rrule').value = "FREQ=DAILY;COUNT=5" - - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # read the event back from the server - org_ev = self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) - - # 2. Add an exception to the master event and invite attendee1 to it - now = datetime.datetime.now(dateutil.tz.gettz("America/Montreal")) - org_ev.vevent.last_modified.value = now - orig_dtstart = org_ev.vevent.dtstart.value - orig_dtend = org_ev.vevent.dtend.value - - ev_exception = org_ev.add("vevent") - ev_exception.add('created').value = now - ev_exception.add('last-modified').value = now - ev_exception.add('dtstamp').value = now - ev_exception.add('uid').value = uid - ev_exception.add('summary').value = summary - # out of laziness, add the exception for the first occurence of the event - recurrence_id = orig_dtstart - ev_exception.add('recurrence-id').value = recurrence_id - - ev_exception.add('transp').value = "OPAQUE" - ev_exception.add('description').value = "Exception" - ev_exception.add('sequence').value = "1" - ev_exception.add('dtstart').value = orig_dtstart - ev_exception.add('dtend').value = orig_dtend - - # 2.1 Add attendee1 and organizer to the exception - organizer = ev_exception.add('organizer') - organizer.cn_param = self.user_name - organizer.partstat_param = "ACCEPTED" - organizer.value = self.user_email - attendee = ev_exception.add('attendee') - attendee.cn_param = self.attendee1_name - attendee.rsvp_param = "TRUE" - attendee.role_param = "REQ-PARTICIPANT" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.attendee1_email - - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), org_ev, - exp_status=204) - - # 3. Make sure the attendee got the event - attendee_ev = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name)) - - for ev in attendee_ev.vevent_list: - try: - if (ev.recurrence_id.value): - attendee_ev_exception = ev - except: - attendee_ev_master = ev - - # make sure sogo doesn't duplicate attendees - yes, we've seen that - self.assertEquals(len(attendee_ev_master.attendee_list), 1) - self.assertEquals(len(attendee_ev_exception.attendee_list), 1) - - # 4. The master event must contain the invitation, declined - self.assertEquals(attendee_ev_master.attendee.partstat_param, "DECLINED") - - # 5. The exception event contain the invitation, NEEDS-ACTION - self.assertEquals(attendee_ev_exception.attendee.partstat_param, "NEEDS-ACTION") - - # 6. attendee accepts invitation - attendee_ev_exception.attendee.partstat_param = "ACCEPTED" - self._putEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), - attendee_ev, exp_status=204) - - # fetch the organizer's event - org_ev = self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) - for ev in org_ev.vevent_list: - try: - if (ev.recurrence_id.value): - org_ev_exception = ev - except: - org_ev_master = ev - - # make sure sogo doesn't duplicate attendees - self.assertEquals(len(org_ev_master.attendee_list), 1) - self.assertEquals(len(org_ev_exception.attendee_list), 1) - - # 7. Make sure organizer got the accept for the exception and - # that the attendee is still declined in the master - self.assertEquals(org_ev_exception.attendee.partstat_param, "ACCEPTED") - self.assertEquals(org_ev_master.attendee.partstat_param, "DECLINED") - - # 8. delete the attendee from the master event (uninvite) - # The event should be deleted from the attendee's calendar - del org_ev_exception.attendee - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), - org_ev, exp_status=204) - del org_ev_master.attendee - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), - org_ev, exp_status=204) - - self._getEvent(self.client, "%s%s" % (self.attendee1_calendar, ics_name), - exp_status=404) - - # now be happy - - def testRruleInvitationDeleteExdate(self): - """RRULE invitation delete exdate dance""" - - # Workflow: - # Create an recurring event and invite Bob - # Add an exdate to the master event - # Verify that the exdate has propagated to Bob's calendar - # Add an exdate to bob's version of the event - # Verify that an exception has been created in the org's calendar - # and that bob is 'declined' - - ics_name = "test-rrule-invitation-deleted-exdate-dance.ics" - self.ics_list += [ics_name] - - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar, ics_name), None) - self._deleteEvent(self.attendee1_client, - "%s%s" % (self.attendee1_calendar, ics_name), None) - - # 1. create a recurring event in the organiser's calendar - summary="Test-rrule-invitation-deleted-exdate-dance" - uid=summary - event = self._newEvent(summary, uid) - event.vevent.add('rrule').value = "FREQ=DAILY;COUNT=5" - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.partstat_param = "ACCEPTED" - organizer.value = self.user_email - attendee = event.vevent.add('attendee') - attendee.cn_param = self.attendee1_name - attendee.rsvp_param = "TRUE" - attendee.role_param = "REQ-PARTICIPANT" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.attendee1_email - - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # 2. Make sure the attendee got it - self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name)) - - # 3. Add exdate to master event - org_ev=self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) - orig_dtstart = org_ev.vevent.dtstart.value - # exdate is a list in vobject.icalendar - org_exdate = [orig_dtstart.astimezone(dateutil.tz.gettz("UTC"))] - org_ev.vevent.add('exdate').value = org_exdate - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), org_ev, exp_status=204) - - # 4. make sure the attendee has the exdate - attendee_ev = self._getEvent(self.attendee1_client, "%s%s" % - (self.attendee1_calendar, ics_name)) - self.assertEqual(org_exdate, attendee_ev.vevent.exdate.value) - - # 5. Create an exdate in the attendee's calendar - new_exdate = orig_dtstart + datetime.timedelta(days=2) - attendee_exdate = [new_exdate.astimezone(dateutil.tz.gettz("UTC"))] - attendee_ev.vevent.add('exdate').value = attendee_exdate - now = datetime.datetime.now(dateutil.tz.gettz("America/Montreal")) - attendee_ev.vevent.last_modified.value = now - self._putEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), - attendee_ev, exp_status=204) - - # 6. Make sure the attendee is: - # needs-action in master event - # declined in the new exception created by the exdate above - org_ev=self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) - for ev in org_ev.vevent_list: - try: - if (ev.recurrence_id.value == attendee_exdate[0]): - org_ev_exception = ev - except: - org_ev_master = ev - - self.assertTrue(org_ev_exception) - # make sure sogo doesn't duplicate attendees - self.assertEquals(len(org_ev_master.attendee_list), 1) - self.assertEquals(len(org_ev_exception.attendee_list), 1) - - self.assertEqual(org_ev_master.attendee.partstat_param, "NEEDS-ACTION"); - self.assertEqual(org_ev_exception.attendee.partstat_param, "DECLINED"); - - def testOrganizerIsAttendee(self): - """ iCal organizer is attendee - bug #1839 """ - - # This tries to have the same behavior as iCal - # 1. create an event, add an attendee and add the organizer as an attendee - # 2. SOGo should remove the organizer from the attendee list - ics_name = "test-organizer-is-attendee.ics" - self.ics_list += [ics_name] - - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar, ics_name), None) - self._deleteEvent(self.attendee1_client, - "%s%s" % (self.attendee1_calendar, ics_name), None) - - # 1. create a recurring event in the organiser's calendar - summary="org is attendee" - uid=summary - event = self._newEvent(summary, uid) - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.partstat_param = "ACCEPTED" - organizer.value = self.user_email - attendee = event.vevent.add('attendee') - attendee.cn_param = self.attendee1_name - attendee.rsvp_param = "TRUE" - attendee.role_param = "REQ-PARTICIPANT" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.attendee1_email - - # 1.1 add the organizer as an attendee - attendee = event.vevent.add('attendee') - attendee.cn_param = self.user_name - attendee.rsvp_param = "TRUE" - attendee.role_param = "REQ-PARTICIPANT" - attendee.partstat_param = "ACCEPTED" - attendee.value = self.user_email - - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # 2. Fetch the event and make sure the organizer is not in the attendee list anymore - org_ev = self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) - - for attendee in org_ev.vevent.attendee_list: - self.assertNotEqual(self.user_email, attendee.value) - - def testEventsWithSameUID(self): - """ PUT 2 events with the same UID - bug #1853 """ - - ics_name = "test-same-uid.ics" - self.ics_list += [ics_name] - - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar, ics_name), None) - - conflict_ics_name = "test-same-uid-conflict.ics" - self.ics_list += [ics_name] - - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar, conflict_ics_name), None) - - # 1. create simple event - summary="same uid" - uid=summary - event = self._newEvent(summary, uid) - - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # PUT the same event with a new filename - should trigger a 409 - self._putEvent(self.client, "%s%s" % (self.user_calendar, conflict_ics_name), event, exp_status=409) - - def testInvitationDelegation(self): - """ invitation delegation """ - - ics_name = "test-delegation.ics" - self.ics_list += [ics_name] - - # the invitation must not exist - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar, ics_name), None) - self._deleteEvent(self.attendee1_client, - "%s%s" % (self.attendee1_calendar, ics_name), None) - self._deleteEvent(self.attendee1_delegate_client, - "%s%s" % (self.attendee1_delegate_calendar, ics_name), None) - - # 1. org -> attendee => org: 1, attendee: 1 (pst=N-A), delegate: 0 - - invitation = self._newEvent() - invitation.add("method").value = "REQUEST" - organizer = invitation.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - attendee = invitation.vevent.add('attendee') - attendee.cn_param = self.attendee1_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.attendee1_email - - self._postEvent(self.client, self.user_calendar, invitation, - self.user_email, [self.attendee1_email]) - del invitation.method - self._putEvent(self.client, - "%stest-delegation.ics" % self.user_calendar, - invitation) - - att_inv = self._getEvent(self.attendee1_client, - "%stest-delegation.ics" - % self.attendee1_calendar) - self._compareAttendees(att_inv, invitation) - - # 2. attendee delegates to delegate - # => org: 1 (updated), attendee: 1 (updated,pst=D), - # delegate: 1 (new,pst=N-A) - - invitation.add("method").value = "REQUEST" - attendee1 = invitation.vevent.attendee - attendee1.partstat_param = "DELEGATED" - attendee1.delegated_to_param = self.attendee1_delegate_email - delegate = invitation.vevent.add('attendee') - delegate.delegated_from_param = self.attendee1_email - delegate.cn_param = self.attendee1_delegate_name - delegate.rsvp_param = "TRUE" - delegate.partstat_param = "NEEDS-ACTION" - delegate.value = self.attendee1_delegate_email - - self._postEvent(self.attendee1_client, - self.attendee1_calendar, invitation, - self.attendee1_email, [self.attendee1_delegate_email]) - invitation.method.value = "REPLY" - self._postEvent(self.attendee1_client, - self.attendee1_calendar, invitation, - self.attendee1_email, [self.user_email]) - del invitation.method - self._putEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar, - invitation, 204) - - del_inv = self._getEvent(self.attendee1_delegate_client, - "%stest-delegation.ics" - % self.attendee1_delegate_calendar) - self._compareAttendees(del_inv, invitation) - org_inv = self._getEvent(self.client, - "%stest-delegation.ics" % self.user_calendar) - self._compareAttendees(org_inv, invitation) - - # 3. delegate accepts - # => org: 1 (updated), attendee: 1 (updated,pst=D), - # delegate: 1 (accepted,pst=A) - - invitation.add("method").value = "REPLY" - delegate.partstat_param = "ACCEPTED" - self._postEvent(self.attendee1_delegate_client, - self.attendee1_delegate_calendar, invitation, - self.attendee1_delegate_email, [self.user_email, self.attendee1_email]) - del invitation.method - self._putEvent(self.attendee1_delegate_client, - "%stest-delegation.ics" % self.attendee1_delegate_calendar, - invitation, 204) - - org_inv = self._getEvent(self.client, - "%stest-delegation.ics" % self.user_calendar) - self._compareAttendees(org_inv, invitation) - att_inv = self._getEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar) - self._compareAttendees(att_inv, invitation) - - # 4. attendee accepts - # => org: 1 (updated), attendee: 1 (updated,pst=A), - # delegate: 0 (cancelled, deleted) - - cancellation = vobject.iCalendar() - cancellation.copy(invitation) - cancellation.add("method").value = "CANCEL" - cancellation.vevent.sequence.value = "1" - self._postEvent(self.attendee1_client, - self.attendee1_calendar, cancellation, - self.attendee1_email, [self.attendee1_delegate_email]) - - attendee1 = invitation.vevent.attendee - attendee1.partstat_param = "ACCEPTED" - del attendee1.delegated_to_param - invitation.add("method").value = "REPLY" - invitation.vevent.remove(delegate) - self._postEvent(self.attendee1_client, - self.attendee1_calendar, invitation, - self.attendee1_email, [self.user_email]) - - del invitation.method - self._putEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar, - invitation, 204) - - org_inv = self._getEvent(self.client, - "%stest-delegation.ics" % self.user_calendar) - self._compareAttendees(org_inv, invitation) - - del_inv = self._getEvent(self.attendee1_delegate_client, - "%stest-delegation.ics" % self.attendee1_delegate_calendar, 404) - - # 5. org updates inv. - # => org: 1 (updated), attendee: 1 (updated), delegate: 0 - - invitation.add("method").value = "REQUEST" - invitation.vevent.summary.value = "Updated invitation" - invitation.vevent.sequence.value = "1" - attendee.partstat_param = "NEEDS-ACTION" - now = datetime.datetime.now() - invitation.vevent.last_modified.value = now - invitation.vevent.dtstamp.value = now - - self._postEvent(self.client, self.user_calendar, invitation, - self.user_email, [self.attendee1_email]) - - del invitation.method - self._putEvent(self.client, - "%stest-delegation.ics" % self.user_calendar, - invitation, 204) - - att_inv = self._getEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar) - self._compareAttendees(att_inv, invitation) - - # 6. attendee delegates to delegate - # => org: 1 (updated), attendee: 1 (updated), delegate: 1 (new) - - invitation.add("method").value = "REQUEST" - attendee1.partstat_param = "DELEGATED" - attendee1.delegated_to_param = self.attendee1_delegate_email - - delegate = invitation.vevent.add('attendee') - delegate.delegated_from_param = self.attendee1_email - delegate.cn_param = self.attendee1_delegate_name - delegate.rsvp_param = "TRUE" - delegate.partstat_param = "NEEDS-ACTION" - delegate.value = self.attendee1_delegate_email - - self._postEvent(self.attendee1_client, - self.attendee1_calendar, invitation, - self.attendee1_email, [self.attendee1_delegate_email]) - invitation.method.value = "REPLY" - self._postEvent(self.attendee1_client, - self.attendee1_calendar, invitation, - self.attendee1_email, [self.user_email]) - del invitation.method - self._putEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar, - invitation, 204) - - org_inv = self._getEvent(self.client, - "%stest-delegation.ics" % self.user_calendar) - self._compareAttendees(org_inv, invitation) - del_inv = self._getEvent(self.attendee1_delegate_client, - "%stest-delegation.ics" - % self.attendee1_delegate_calendar) - self._compareAttendees(del_inv, invitation) - - # 7. delegate accepts - # => org: 1 (updated), attendee: 1 (updated), delegate: 1 (accepted) - - invitation.add("method").value = "REPLY" - delegate.partstat_param = "ACCEPTED" - self._postEvent(self.attendee1_delegate_client, - self.attendee1_delegate_calendar, invitation, - self.attendee1_delegate_email, [self.user_email, - self.attendee1_email]) - del invitation.method - self._putEvent(self.attendee1_delegate_client, - "%stest-delegation.ics" % self.attendee1_delegate_calendar, - invitation, 204) - - org_inv = self._getEvent(self.client, - "%stest-delegation.ics" % self.user_calendar) - self._compareAttendees(org_inv, invitation) - att_inv = self._getEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar) - self._compareAttendees(att_inv, invitation) - - # 8. org updates inv. - # => org: 1 (updated), attendee: 1 (updated,partstat unchanged), - # delegate: 1 (updated,partstat reset) - - invitation.add("method").value = "REQUEST" - now = datetime.datetime.now() - invitation.vevent.last_modified.value = now - invitation.vevent.dtstamp.value = now - invitation.vevent.summary.value = "Updated invitation (again)" - invitation.vevent.sequence.value = "2" - delegate.partstat_param = "NEEDS-ACTION" - - self._postEvent(self.client, self.user_calendar, invitation, - self.user_email, [self.attendee1_email, self.attendee1_delegate_email]) - - del invitation.method - self._putEvent(self.client, - "%stest-delegation.ics" % self.user_calendar, - invitation, 204) - - att_inv = self._getEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar) - self._compareAttendees(att_inv, invitation) - del_inv = self._getEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar) - self._compareAttendees(del_inv, invitation) - - # 9. org cancels invitation - # => org: 1 (updated), attendee: 0 (cancelled, deleted), - # delegate: 0 (cancelled, deleted) - - invitation.add("method").value = "CANCEL" - now = datetime.datetime.now() - invitation.vevent.last_modified.value = now - invitation.vevent.dtstamp.value = now - invitation.vevent.summary.value = "Cancelled invitation (again)" - invitation.vevent.sequence.value = "3" - - self._postEvent(self.client, self.user_calendar, invitation, - self.user_email, [self.attendee1_email, self.attendee1_delegate_email]) - - del invitation.method - invitation.vevent.remove(attendee) - invitation.vevent.remove(delegate) - self._putEvent(self.client, - "%stest-delegation.ics" % self.user_calendar, - invitation, 204) - - att_inv = self._getEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar, 404) - del_inv = self._getEvent(self.attendee1_delegate_client, - "%stest-delegation.ics" % self.attendee1_delegate_calendar, 404) - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-config.py b/Tests/Integration/test-config.py deleted file mode 100755 index c76af88b2..000000000 --- a/Tests/Integration/test-config.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/python - -from config import hostname, port, username, password, mailserver, subscriber_username, attendee1, attendee1_delegate - -import sogotests -import unittest -import time - -class CalDAVITIPDelegationTest(unittest.TestCase): - def testConfigPY(self): - """ config.py validation """ - try: - test = hostname - except: - self.fail("'hostname' is not defined") - - try: - test = username - except: - self.fail("'username' is not defined") - - try: - test = subscriber_username - except: - self.fail("'subscriber_username' is not defined") - - try: - test = attendee1 - except: - self.fail("'attendee1' is not defined") - - try: - test = attendee1_delegate - except: - self.fail("'attendee1_delegate' is not defined") - - self.assertEquals(subscriber_username, attendee1, - "'subscriber_username' and 'attendee1'" - + " must be the same user") - - try: - test = mailserver - except: - self.fail("'mailserver' is not defined") - - userHash = {} - userList = [ username, subscriber_username, attendee1_delegate ] - for user in userList: - self.assertFalse(userHash.has_key(user), - "username, attendee1, attendee1_delegate must" - + " all be different users ('%s')" - % user) - userHash[user] = True - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-contact-categories.py b/Tests/Integration/test-contact-categories.py deleted file mode 100755 index df5734243..000000000 --- a/Tests/Integration/test-contact-categories.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/python - -import sogotests -import unittest -import webdavlib - -from config import * - -class HTTPContactCategoriesTest(unittest.TestCase): - def _setCategories(self, user, categories = None): - resource = '/SOGo/dav/%s/Contacts/' % user - if categories is None: - categories = [] - elements = [ { "{urn:inverse:params:xml:ns:inverse-dav}category": x } - for x in categories ] - props = { "{urn:inverse:params:xml:ns:inverse-dav}contacts-categories": elements } - proppatch = webdavlib.WebDAVPROPPATCH(resource, props) - client = webdavlib.WebDAVClient(hostname, port, username, password) - client.execute(proppatch) - self.assertEquals(proppatch.response["status"], 207, - "failure (%s) setting '%s' categories on %s's contacts" - % (proppatch.response["status"], - "', '".join(categories), user)) - - def _getCategories(self, user): - resource = '/SOGo/dav/%s/Contacts/' % user - props = [ "{urn:inverse:params:xml:ns:inverse-dav}contacts-categories" ] - propfind = webdavlib.WebDAVPROPFIND(resource, props, "0") - client = webdavlib.WebDAVClient(hostname, port, username, password) - client.execute(propfind) - self.assertEquals(propfind.response["status"], 207, - "failure (%s) getting categories on %s's contacts" - % (propfind.response["status"], user)) - - categories = [] - prop_nodes = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat/{DAV:}prop/{urn:inverse:params:xml:ns:inverse-dav}contacts-categories") - for prop_node in prop_nodes: - cat_nodes = prop_node.findall("{urn:inverse:params:xml:ns:inverse-dav}category") - if cat_nodes is not None: - for cat_node in cat_nodes: - categories.append(cat_node.text) - - return categories - - - def test(self): - self._setCategories(username, []) - cats = self._getCategories(username) - self.assertTrue(cats is not None and len(cats) == 0) - - self._setCategories(username, [ "Coucou" ]) - cats = self._getCategories(username) - self.assertTrue(cats is not None and len(cats) == 1) - self.assertEquals(cats[0], "Coucou") - - self._setCategories(username, [ "Toto", "Cuicui" ]) - cats = self._getCategories(username) - self.assertTrue(cats is not None and len(cats) == 2) - self.assertEquals(cats[0], "Toto") - self.assertEquals(cats[1], "Cuicui") - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-davacl.py b/Tests/Integration/test-davacl.py deleted file mode 100755 index b15222ad2..000000000 --- a/Tests/Integration/test-davacl.py +++ /dev/null @@ -1,1130 +0,0 @@ -#!/usr/bin/python - -from config import hostname, port, username, password, subscriber_username, subscriber_password, \ - superuser, superuser_password - -import sys -import unittest -import webdavlib -import time - -import sogotests -import utilities - -# TODO: -# - cal: complete test for "modify": "respond to" causes a 204 but no actual -# modification should occur -# - ab: testcase for addressbook-query, webdav-sync (no "calendar-data" -# equivalent) -# - cal: testcase for "calendar-query" -# - test rights validity: -# - send invalid rights to SOGo and expect failures -# - refetch the set of rights and make sure it matches what was set -# originally -# - test "current-user-acl-set" - -class DAVCalendarSuperUserAclTest(unittest.TestCase): - def __init__(self, arg): - self.client = webdavlib.WebDAVClient(hostname, port, - superuser, superuser_password) - self.resource = "/SOGo/dav/%s/Calendar/test-dav-superuser-acl/" % subscriber_username - self.filename = "suevent.ics" - self.url = "%s%s" % (self.resource, self.filename) - - unittest.TestCase.__init__(self, arg) - - def setUp(self): - delete = webdavlib.WebDAVDELETE(self.resource) - self.client.execute(delete) - mkcol = webdavlib.WebDAVMKCOL(self.resource) - self.client.execute(mkcol) - self.assertEquals(mkcol.response["status"], 201, - "preparation: failure creating collection" - "(code = %d)" % mkcol.response["status"]) - - def tearDown(self): - delete = webdavlib.WebDAVDELETE(self.resource) - self.client.execute(delete) - - def _getEvent(self): - get = webdavlib.HTTPGET(self.url) - self.client.execute(get) - - if get.response["status"] == 200: - event = get.response["body"].replace("\r", "") - else: - event = None - - return event - - def _calendarDataInMultistatus(self, query, response_tag = "{DAV:}response"): - event = None - - # print "\n\n\n%s\n\n" % query.response["body"] - # print "\n\n" - response_nodes = query.response["document"].findall(response_tag) - for response_node in response_nodes: - href_node = response_node.find("{DAV:}href") - href = href_node.text - if href.endswith(self.filename): - propstat_node = response_node.find("{DAV:}propstat") - if propstat_node is not None: - status_node = propstat_node.find("{DAV:}status") - status = status_node.text - if status.endswith("200 OK"): - data_node = propstat_node.find("{DAV:}prop/{urn:ietf:params:xml:ns:caldav}calendar-data") - event = data_node.text - elif not (status.endswith("404 Resource Not Found") - or status.endswith("404 Not Found")): - self.fail("%s: unexpected status code: '%s'" - % (self.filename, status)) - - return event - - def _propfindEvent(self): - propfind = webdavlib.WebDAVPROPFIND(self.resource, - ["{urn:ietf:params:xml:ns:caldav}calendar-data"], - 1) - self.client.execute(propfind) - if propfind.response["status"] != 404: - event = self._calendarDataInMultistatus(propfind) - - return event - - def _multigetEvent(self): - event = None - - multiget = webdavlib.CalDAVCalendarMultiget(self.resource, - ["{urn:ietf:params:xml:ns:caldav}calendar-data"], - [ self.url ]) - self.client.execute(multiget) - if multiget.response["status"] != 404: - event = self._calendarDataInMultistatus(multiget) - - return event - - def _webdavSyncEvent(self): - event = None - - sync_query = webdavlib.WebDAVSyncQuery(self.resource, None, - ["{urn:ietf:params:xml:ns:caldav}calendar-data"]) - self.client.execute(sync_query) - if sync_query.response["status"] != 404: - event = self._calendarDataInMultistatus(sync_query, "{DAV:}response") - - return event - - def testSUAccess(self): - """create, read, modify, delete for superuser""" - event = event_template % { "class": "PUBLIC", - "filename": self.filename, - "organizer_line": "", - "attendee_line": "" } - - # 1. Create - put = webdavlib.HTTPPUT(self.url, event) - put.content_type = "text/calendar; charset=utf-8" - self.client.execute(put) - self.assertEquals(put.response["status"], 201, - "%s: event creation/modification:" - " expected status code '201' (received '%d')" - % (self.filename, put.response["status"])) - - # 2. Read - readEvent = self._getEvent() - self.assertEquals(readEvent, event, - "GET: returned event does not match") - readEvent = self._propfindEvent() - self.assertEquals(readEvent, event, - "PROPFIND: returned event does not match") - readEvent = self._multigetEvent() - self.assertEquals(readEvent, event, - "MULTIGET: returned event does not match") - readEvent = self._webdavSyncEvent() - self.assertEquals(readEvent, event, - "WEBDAV-SYNC: returned event does not match") - - # 3. Modify - for eventClass in [ "CONFIDENTIAL", "PRIVATE", "PUBLIC" ]: - event = event_template % { "class": eventClass, - "filename": self.filename, - "organizer_line": "", - "attendee_line": "" } - put = webdavlib.HTTPPUT(self.url, event) - put.content_type = "text/calendar; charset=utf-8" - self.client.execute(put) - self.assertEquals(put.response["status"], 204, - "%s: event modification failed" - " expected status code '204' (received '%d')" - % (self.filename, put.response["status"])) - - # 4. Delete - delete = webdavlib.WebDAVDELETE(self.url) - self.client.execute(delete) - self.assertEquals(delete.response["status"], 204, - "%s: event deletion failed" - " expected status code '204' (received '%d')" - % (self.filename, put.response["status"])) - -class DAVAclTest(unittest.TestCase): - resource = None - - def __init__(self, arg): - self.client = webdavlib.WebDAVClient(hostname, port, - username, password) - unittest.TestCase.__init__(self, arg) - - def setUp(self): - delete = webdavlib.WebDAVDELETE(self.resource) - self.client.execute(delete) - mkcol = webdavlib.WebDAVMKCOL(self.resource) - self.client.execute(mkcol) - self.assertEquals(mkcol.response["status"], 201, - "preparation: failure creating collection" - "(code = %d)" % mkcol.response["status"]) - self.subscriber_client = webdavlib.WebDAVClient(hostname, port, - subscriber_username, - subscriber_password) - - def tearDown(self): - delete = webdavlib.WebDAVDELETE(self.resource) - self.client.execute(delete) - - def _versitLine(self, previous_key, line): - if line[:1] == ' ' and previous_key: - key, value = (previous_key, line[1:]) - else: - key, value = line.split(":") - semicolon = key.find(";") - if semicolon > -1: - key = key[:semicolon] - - return (key, value) - - def versitDict(self, event): - versitStruct = {} - key = '' - for line in event.splitlines(): - (key, value) = self._versitLine(key, line) - if not (key == "BEGIN" or key == "UID" or key == "END"): - if key in versitStruct: - versitStruct[key] += value - else: - versitStruct[key] = value - - return versitStruct - -event_template = """BEGIN:VCALENDAR -PRODID:-//Inverse//Event Generator//EN -VERSION:2.0 -BEGIN:VEVENT -SEQUENCE:0 -TRANSP:OPAQUE -UID:12345-%(class)s-%(filename)s -SUMMARY:%(class)s event (orig. title) -DTSTART:20090805T100000Z -DTEND:20090805T140000Z -CLASS:%(class)s -DESCRIPTION:%(class)s description -LOCATION:location -%(organizer_line)s%(attendee_line)sCREATED:20090805T100000Z -DTSTAMP:20090805T100000Z -END:VEVENT -END:VCALENDAR""" - -task_template = """BEGIN:VCALENDAR -PRODID:-//Inverse//Event Generator//EN -VERSION:2.0 -BEGIN:VTODO -CREATED:20100122T201440Z -LAST-MODIFIED:20100201T175246Z -DTSTAMP:20100201T175246Z -UID:12345-%(class)s-%(filename)s -SUMMARY:%(class)s event (orig. title) -CLASS:%(class)s -DESCRIPTION:%(class)s description -STATUS:IN-PROCESS -PERCENT-COMPLETE:0 -END:VTODO -END:VCALENDAR""" - -class DAVCalendarAclTest(DAVAclTest): - resource = '/SOGo/dav/%s/Calendar/test-dav-acl/' % username - user_email = None - - def __init__(self, arg): - DAVAclTest.__init__(self, arg) - self.acl_utility = utilities.TestCalendarACLUtility(self, - self.client, - self.resource) - - def setUp(self): - DAVAclTest.setUp(self) - self.user_email = self.acl_utility.fetchUserInfo(username)[1] - self.classToICSClass = { "pu": "PUBLIC", - "pr": "PRIVATE", - "co": "CONFIDENTIAL" } - self._putEvent(self.client, "public-event.ics", "PUBLIC") - self._putEvent(self.client, "private-event.ics", "PRIVATE") - self._putEvent(self.client, "confidential-event.ics", "CONFIDENTIAL") - self._putTask(self.client, "public-task.ics", "PUBLIC") - self._putTask(self.client, "private-task.ics", "PRIVATE") - self._putTask(self.client, "confidential-task.ics", "CONFIDENTIAL") - - def testViewAllPublic(self): - """'view all' on a specific class (PUBLIC)""" - self._testRights({ "pu": "v" }) - - def testModifyPublicViewAllPrivateViewDConfidential(self): - """'modify' PUBLIC, 'view all' PRIVATE, 'view d&t' confidential""" - self._testRights({ "pu": "m", "pr": "v", "co": "d" }) - - def testCreateOnly(self): - """'create' only""" - self._testRights({ "c": True }) - - def testDeleteOnly(self): - """'delete' only""" - self._testRights({ "d": True }) - - def testCreateDeleteModifyPublicViewAllPrivateViewDConfidential(self): - """'create', 'delete', 'view d&t' PUBLIC, 'modify' PRIVATE""" - self._testRights({ "c": True, "d": True, "pu": "d", "pr": "m" }) - - def testCreateRespondToPublic(self): - """'create', 'respond to' PUBLIC""" - self._testRights({ "c": True, "pu": "r" }) - - def testNothing(self): - """no right given""" - self._testRights({}) - - def _putEvent(self, client, filename, - event_class = "PUBLIC", - exp_status = 201, - organizer = None, attendee = None, - partstat = "NEEDS-ACTION"): - url = "%s%s" % (self.resource, filename) - if organizer is not None: - organizer_line = "ORGANIZER:%s\n" % organizer - else: - organizer_line = "" - if attendee is not None: - attendee_line = "ATTENDEE;PARTSTAT=%s:%s\n" % (partstat, attendee) - else: - attendee_line = "" - event = event_template % { "class": event_class, - "filename": filename, - "organizer_line": organizer_line, - "attendee_line": attendee_line } - put = webdavlib.HTTPPUT(url, event) - put.content_type = "text/calendar; charset=utf-8" - client.execute(put) - self.assertEquals(put.response["status"], exp_status, - "%s: event creation/modification:" - " expected status code '%d' (received '%d')" - % (filename, exp_status, put.response["status"])) - - def _putTask(self, client, filename, - task_class = "PUBLIC", - exp_status = 201): - url = "%s%s" % (self.resource, filename) - task = task_template % { "class": task_class, - "filename": filename } - put = webdavlib.HTTPPUT(url, task) - put.content_type = "text/calendar; charset=utf-8" - client.execute(put) - self.assertEquals(put.response["status"], exp_status, - "%s: task creation/modification:" - " expected status code '%d' (received '%d')" - % (filename, exp_status, put.response["status"])) - - def _deleteEvent(self, client, filename, exp_status = 204): - url = "%s%s" % (self.resource, filename) - delete = webdavlib.WebDAVDELETE(url) - client.execute(delete) - self.assertEquals(delete.response["status"], exp_status, - "%s: event deletion: expected status code '%d'" - " (received '%d')" - % (filename, exp_status, delete.response["status"])) - - def _currentUserPrivilegeSet(self, resource, expStatus = 207): - propfind = webdavlib.WebDAVPROPFIND(resource, - ["{DAV:}current-user-privilege-set"], - 0) - self.subscriber_client.execute(propfind) - self.assertEquals(propfind.response["status"], expStatus, - "unexected status code when reading privileges:" - + " %s instead of %d" - % (propfind.response["status"], expStatus)) - - privileges = [] - if expStatus < 300: - response_nodes = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}current-user-privilege-set/{DAV:}privilege") - for node in response_nodes: - privileges.extend([x.tag for x in node.getchildren()]) - - return privileges - - def _comparePrivilegeSets(self, expectedPrivileges, privileges): - testHash = dict(map(lambda x: (x, True), privileges)) - for privilege in expectedPrivileges: - self.assertTrue(testHash.has_key(privilege), - "expected privilege '%s' not found" % privilege) - testHash = dict(map(lambda x: (x, True), expectedPrivileges)) - for privilege in privileges: - self.assertTrue(testHash.has_key(privilege), - "excessive privilege '%s' found" % privilege) - - def _testCollectionDAVAcl(self, rights): - if len(rights) > 0: - expectedPrivileges = ['{DAV:}read', - '{DAV:}read-current-user-privilege-set', - '{urn:ietf:params:xml:ns:caldav}read-free-busy'] - else: - expectedPrivileges = [] - if rights.has_key("c"): - extraPrivileges = ["{DAV:}bind", - "{DAV:}write-content", - '{urn:ietf:params:xml:ns:caldav}schedule', - '{urn:ietf:params:xml:ns:caldav}schedule-post', - '{urn:ietf:params:xml:ns:caldav}schedule-post-vevent', - '{urn:ietf:params:xml:ns:caldav}schedule-post-vtodo', - '{urn:ietf:params:xml:ns:caldav}schedule-post-vjournal', - '{urn:ietf:params:xml:ns:caldav}schedule-post-vfreebusy', - '{urn:ietf:params:xml:ns:caldav}schedule-deliver', - '{urn:ietf:params:xml:ns:caldav}schedule-deliver-vevent', - '{urn:ietf:params:xml:ns:caldav}schedule-deliver-vtodo', - '{urn:ietf:params:xml:ns:caldav}schedule-deliver-vjournal', - '{urn:ietf:params:xml:ns:caldav}schedule-deliver-vfreebusy', - '{urn:ietf:params:xml:ns:caldav}schedule-respond', - '{urn:ietf:params:xml:ns:caldav}schedule-respond-vevent', - '{urn:ietf:params:xml:ns:caldav}schedule-respond-vtodo'] - expectedPrivileges.extend(extraPrivileges) - if rights.has_key("d"): - expectedPrivileges.append("{DAV:}unbind") - if len(expectedPrivileges) == 0: - expStatus = 404 - else: - expStatus = 207 - privileges = self._currentUserPrivilegeSet(self.resource, expStatus) - - # When comparing privileges on DAV collection, we remove all 'default' - # privileges on the collection. - privileges_set = set(privileges); - for x in ("public", "private", "confidential"): - privileges_set.discard("{urn:inverse:params:xml:ns:inverse-dav}viewwhole-%s-records" % x) - privileges_set.discard("{urn:inverse:params:xml:ns:inverse-dav}viewdant-%s-records" % x) - privileges_set.discard("{urn:inverse:params:xml:ns:inverse-dav}modify-%s-records" % x) - privileges_set.discard("{urn:inverse:params:xml:ns:inverse-dav}respondto-%s-records" %x) - - self._comparePrivilegeSets(expectedPrivileges, list(privileges_set)) - - def _testEventDAVAcl(self, event_class, right, error_code): - icsClass = self.classToICSClass[event_class].lower() - for suffix in [ "event", "task" ]: - url = "%s%s-%s.ics" % (self.resource, icsClass, suffix) - - if right is None: - expStatus = error_code - expectedPrivileges = None - else: - expStatus = 207 - expectedPrivileges = ['{DAV:}read-current-user-privilege-set', - '{urn:inverse:params:xml:ns:inverse-dav}view-date-and-time', - '{DAV:}read'] - if right != "d": - extraPrivilege = '{urn:inverse:params:xml:ns:inverse-dav}view-whole-component' - expectedPrivileges.append(extraPrivilege) - if right != "v": - extraPrivileges = ['{urn:inverse:params:xml:ns:inverse-dav}respond-to-component', - '{DAV:}write-content'] - expectedPrivileges.extend(extraPrivileges) - if right != "r": - extraPrivileges = ['{DAV:}write-properties', - '{DAV:}write'] - expectedPrivileges.extend(extraPrivileges) - - privileges = self._currentUserPrivilegeSet(url, expStatus) - if expStatus != error_code: - self._comparePrivilegeSets(expectedPrivileges, privileges) - - def _testRights(self, rights): - self.acl_utility.setupRights(subscriber_username, rights) - self._testCreate(rights) - self._testCollectionDAVAcl(rights) - self._testEventRight("pu", rights) - self._testEventRight("pr", rights) - self._testEventRight("co", rights) - self._testDelete(rights) - - def _testCreate(self, rights): - if rights.has_key("c") and rights["c"]: - exp_code = 201 - elif len(rights) == 0: - exp_code = 404 - else: - exp_code = 403 - self._putEvent(self.subscriber_client, "creation-test.ics", "PUBLIC", - exp_code) - - def _testDelete(self, rights): - if rights.has_key("d") and rights["d"]: - exp_code = 204 - elif len(rights) == 0: - exp_code = 404 - else: - exp_code = 403 - self._deleteEvent(self.subscriber_client, "public-event.ics", - exp_code) - self._deleteEvent(self.subscriber_client, "private-event.ics", - exp_code) - self._deleteEvent(self.subscriber_client, "confidential-event.ics", - exp_code) - - def _testEventRight(self, event_class, rights): - if rights.has_key(event_class): - right = rights[event_class] - else: - right = None - - event = self._getEvent(event_class) - self._checkViewEventRight("GET", event, event_class, right) - event = self._propfindEvent(event_class) - self._checkViewEventRight("PROPFIND", event, event_class, right) - event = self._multigetEvent(event_class) - self._checkViewEventRight("multiget", event, event_class, right) - event = self._webdavSyncEvent(event_class) - self._checkViewEventRight("webdav-sync", event, event_class, right) - - if len(rights) > 0: - error_code = 403 - else: - error_code = 404 - self._testModify(event_class, right, error_code) - self._testRespondTo(event_class, right, error_code) - self._testEventDAVAcl(event_class, right, error_code) - - def _getEvent(self, event_class, is_invitation = False): - icsClass = self.classToICSClass[event_class] - if is_invitation: - filename = "invitation-%s" % icsClass.lower() - else: - filename = "%s" % icsClass.lower() - url = "%s%s-event.ics" % (self.resource, filename) - get = webdavlib.HTTPGET(url) - self.subscriber_client.execute(get) - - if get.response["status"] == 200: - event = get.response["body"].replace("\r", "") - else: - event = None - - return event - - def _getTask(self, task_class): - filename = "%s" % self.classToICSClass[task_class].lower() - url = "%s%s-task.ics" % (self.resource, filename) - get = webdavlib.HTTPGET(url) - self.subscriber_client.execute(get) - - if get.response["status"] == 200: - task = get.response["body"] - else: - task = None - - return task - - def _calendarDataInMultistatus(self, query, filename, - response_tag = "{DAV:}response"): - event = None - - # print "\n\n\n%s\n\n" % query.response["body"] - # print "\n\n" - response_nodes = query.response["document"].findall("%s" % response_tag) - for response_node in response_nodes: - href_node = response_node.find("{DAV:}href") - href = href_node.text - if href.endswith(filename): - propstat_node = response_node.find("{DAV:}propstat") - if propstat_node is not None: - status_node = propstat_node.find("{DAV:}status") - status = status_node.text - if status.endswith("200 OK"): - data_node = propstat_node.find("{DAV:}prop/{urn:ietf:params:xml:ns:caldav}calendar-data") - event = data_node.text - elif not (status.endswith("404 Resource Not Found") - or status.endswith("404 Not Found")): - self.fail("%s: unexpected status code: '%s'" - % (filename, status)) - - return event - - def _propfindEvent(self, event_class): - event = None - - icsClass = self.classToICSClass[event_class] - filename = "%s-event.ics" % icsClass.lower() - propfind = webdavlib.WebDAVPROPFIND(self.resource, - ["{urn:ietf:params:xml:ns:caldav}calendar-data"], - 1) - self.subscriber_client.execute(propfind) - if propfind.response["status"] != 404: - event = self._calendarDataInMultistatus(propfind, filename) - - return event - - def _multigetEvent(self, event_class): - event = None - - icsClass = self.classToICSClass[event_class] - url = "%s%s-event.ics" % (self.resource, icsClass.lower()) - multiget = webdavlib.CalDAVCalendarMultiget(self.resource, - ["{urn:ietf:params:xml:ns:caldav}calendar-data"], - [ url ]) - self.subscriber_client.execute(multiget) - if multiget.response["status"] != 404: - event = self._calendarDataInMultistatus(multiget, url) - - return event - - def _webdavSyncEvent(self, event_class): - event = None - - icsClass = self.classToICSClass[event_class] - url = "%s%s-event.ics" % (self.resource, icsClass.lower()) - sync_query = webdavlib.WebDAVSyncQuery(self.resource, None, - ["{urn:ietf:params:xml:ns:caldav}calendar-data"]) - self.subscriber_client.execute(sync_query) - if sync_query.response["status"] != 404: - event = self._calendarDataInMultistatus(sync_query, url, - "{DAV:}response") - - return event - - def _checkViewEventRight(self, operation, event, event_class, right): - if right is None: - self.assertEquals(event, None, - "None right expecting event invisibility for" - " operation '%s'" % operation) - else: - self.assertTrue(event is not None, - "no event returned during operation '%s'" - " (right: %s)" % (operation, right)) - if right == "v" or right == "r" or right == "m": - icsClass = self.classToICSClass[event_class] - complete_event = (event_template % {"class": icsClass, - "filename": "%s-event.ics" % icsClass.lower(), - "organizer_line": "", - "attendee_line": ""}) - self.assertTrue(event.strip() == complete_event.strip(), - "Right '%s' should return complete event" - " during operation '%s'" - % (right, operation)) - elif right == "d": - self._testEventIsSecureVersion(event_class, event) - else: - self.fail("Right '%s' is not supported" % right) - - def _testEventIsSecureVersion(self, event_class, event): - icsClass = self.classToICSClass[event_class] - expected_dict = { "VERSION": "2.0", - "PRODID": "-//Inverse//Event Generator//EN", - "SUMMARY": "(%s event)" % icsClass.capitalize(), - "DTSTART": "20090805T100000Z", - "DTEND": "20090805T140000Z", - "DTSTAMP": "20090805T100000Z", - "X-SOGO-SECURE": "YES" } - event_dict = self.versitDict(event) - for key in event_dict.keys(): - self.assertTrue(expected_dict.has_key(key), - "key '%s' of secure event not expected" % key) - self.assertTrue(expected_dict[key] == event_dict[key] - or key == "SUMMARY", - "value for key '%s' of secure does not match" - " (exp: '%s', obtained: '%s'" - % (key, expected_dict[key], event_dict[key] )) - - for key in expected_dict.keys(): - self.assertTrue(event_dict.has_key(key), - "expected key '%s' not found in secure event" - % key) - - def _testModify(self, event_class, right, error_code): - if right == "m" or right == "r": - exp_code = 204 - else: - exp_code = error_code - icsClass = self.classToICSClass[event_class] - filename = "%s-event.ics" % icsClass.lower() - self._putEvent(self.subscriber_client, filename, icsClass, - exp_code) - - def _testRespondTo(self, event_class, right, error_code): - icsClass = self.classToICSClass[event_class] - filename = "invitation-%s-event.ics" % icsClass.lower() - self._putEvent(self.client, filename, icsClass, - 201, - "mailto:nobody@somewhere.com", self.user_email, - "NEEDS-ACTION") - - if right == "m" or right == "r": - exp_code = 204 - else: - exp_code = error_code - - # here we only do 'passive' validation: if a user has a "respond to" - # right, only the attendee entry will me modified. The change of - # organizer must thus be silently ignored below. - self._putEvent(self.subscriber_client, filename, icsClass, - exp_code, "mailto:someone@nowhere.com", self.user_email, - "ACCEPTED") - if exp_code == 204: - att_line = "ATTENDEE;PARTSTAT=ACCEPTED:%s\n" % self.user_email - if right == "r": - exp_event = event_template % {"class": icsClass, - "filename": filename, - "organizer_line": "ORGANIZER;CN=nobody@somewhere.com:mailto:nobody@somewhere.com\n", - "attendee_line": att_line} - else: - exp_event = event_template % {"class": icsClass, - "filename": filename, - "organizer_line": "ORGANIZER;CN=someone@nowhere.com:mailto:someone@nowhere.com\n", - "attendee_line": att_line} - event = self._getEvent(event_class, True) - ics_diff = utilities.ics_compare(exp_event, event) - self.assertTrue(ics_diff.areEqual(), - "'respond to' event does not match:\n" - "Diff(expected, got):\n %s" % ics_diff.textDiff()) - -class DAVAddressBookAclTest(DAVAclTest): - resource = '/SOGo/dav/%s/Contacts/test-dav-acl/' % username - cards = { "new.vcf": """BEGIN:VCARD -VERSION:3.0 -PRODID:-//Inverse//Card Generator//EN -UID:NEWTESTCARD -N:New;Carte -FN:Carte 'new' -ORG:societe;service -NICKNAME:surnom -ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc -ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso -TEL;TYPE=work:+1 514 123-3372 -TEL;TYPE=home:tel dom -TEL;TYPE=cell:portable -TEL;TYPE=fax:fax -TEL;TYPE=pager:pager -X-MOZILLA-HTML:FALSE -EMAIL;TYPE=work:address.email@domaine.ca -EMAIL;TYPE=home:address.email@domaine2.com -URL;TYPE=home:web perso -TITLE:fonction -URL;TYPE=work:page soc -CUSTOM1:divers1 -CUSTOM2:divers2 -CUSTOM3:divers3 -CUSTOM4:divers4 -NOTE:Remarque -X-AIM:pseudo aim -END:VCARD""", - "old.vcf": """BEGIN:VCARD -VERSION:3.0 -PRODID:-//Inverse//Card Generator//EN -UID:NEWTESTCARD -N:Old;Carte -FN:Carte 'old' -ORG:societe;service -NICKNAME:surnom -ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc -ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso -TEL;TYPE=work:+1 514 123-3372 -TEL;TYPE=home:tel dom -TEL;TYPE=cell:portable -TEL;TYPE=fax:fax -TEL;TYPE=pager:pager -X-MOZILLA-HTML:FALSE -EMAIL;TYPE=work:address.email@domaine.ca -EMAIL;TYPE=home:address.email@domaine2.com -URL;TYPE=home:web perso -TITLE:fonction -URL;TYPE=work:page soc -CUSTOM1:divers1 -CUSTOM2:divers2 -CUSTOM3:divers3 -CUSTOM4:divers4 -NOTE:Remarque -X-AIM:pseudo aim -END:VCARD""", - "new-modified.vcf": """BEGIN:VCARD -VERSION:3.0 -PRODID:-//Inverse//Card Generator//EN -UID:NEWTESTCARD -N:New;Carte modifiee -FN:Carte modifiee 'new' -ORG:societe;service -NICKNAME:surnom -ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc -ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso -TEL;TYPE=work:+1 514 123-3372 -TEL;TYPE=home:tel dom -TEL;TYPE=cell:portable -TEL;TYPE=fax:fax -TEL;TYPE=pager:pager -X-MOZILLA-HTML:FALSE -EMAIL;TYPE=work:address.email@domaine.ca -EMAIL;TYPE=home:address.email@domaine2.com -URL;TYPE=home:web perso -TITLE:fonction -URL;TYPE=work:page soc -CUSTOM1:divers1 -CUSTOM2:divers2 -CUSTOM3:divers3 -CUSTOM4:divers4 -NOTE:Remarque -X-AIM:pseudo aim -END:VCARD""", - "old-modified.vcf": """BEGIN:VCARD -VERSION:3.0 -PRODID:-//Inverse//Card Generator//EN -UID:NEWTESTCARD -N:Old;Carte modifiee -FN:Carte modifiee 'old' -ORG:societe;service -NICKNAME:surnom -ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc -ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso -TEL;TYPE=work:+1 514 123-3372 -TEL;TYPE=home:tel dom -TEL;TYPE=cell:portable -TEL;TYPE=fax:fax -TEL;TYPE=pager:pager -X-MOZILLA-HTML:FALSE -EMAIL;TYPE=work:address.email@domaine.ca -EMAIL;TYPE=home:address.email@domaine2.com -URL;TYPE=home:web perso -TITLE:fonction -URL;TYPE=work:page soc -CUSTOM1:divers1 -CUSTOM2:divers2 -CUSTOM3:divers3 -CUSTOM4:divers4 -NOTE:Remarque -X-AIM:pseudo aim -END:VCARD""" } - - def __init__(self, arg): - DAVAclTest.__init__(self, arg) - self.acl_utility = utilities.TestAddressBookACLUtility(self, - self.client, - self.resource) - - def setUp(self): - DAVAclTest.setUp(self) - self._putCard(self.client, "old.vcf", 201) - - def testView(self): - """'view' only""" - self._testRights({ "v": True }) - - def testEdit(self): - """'edit' only""" - self._testRights({ "e": True }) - - def testCreateOnly(self): - """'create' only""" - self._testRights({ "c": True }) - - def testDeleteOnly(self): - """'delete' only""" - self._testRights({ "d": True }) - - def testCreateDelete(self): - """'create', 'delete'""" - self._testRights({ "c": True, "d": True }) - - def testViewCreate(self): - """'view' and 'create'""" - self._testRights({ "c": True, "v": True }) - - def testViewDelete(self): - """'view' and 'delete'""" - self._testRights({ "d": True, "v": True }) - - def testEditCreate(self): - """'edit' and 'create'""" - self._testRights({ "c": True, "e": True }) - - def testEditDelete(self): - """'edit' and 'delete'""" - self._testRights({ "d": True, "e": True }) - - def _testRights(self, rights): - self.acl_utility.setupRights(subscriber_username, rights) - self._testCreate(rights) - self._testView(rights) - self._testEdit(rights) - self._testDelete(rights) - - def _putCard(self, client, filename, exp_status, real_card = None): - url = "%s%s" % (self.resource, filename) - if real_card is None: - real_card = filename - card = self.cards[real_card] - put = webdavlib.HTTPPUT(url, card) - put.content_type = "text/x-vcard; charset=utf-8" - client.execute(put) - self.assertEquals(put.response["status"], exp_status, - "%s: card creation/modification:" - " expected status code '%d' (received '%d')" - % (filename, exp_status, put.response["status"])) - - def _getCard(self, client, filename, exp_status): - url = "%s%s" % (self.resource, filename) - get = webdavlib.HTTPGET(url) - client.execute(get) - self.assertEquals(get.response["status"], exp_status, - "%s: card get:" - " expected status code '%d' (received '%d')" - % (filename, exp_status, get.response["status"])) - - def _deleteCard(self, client, filename, exp_status): - url = "%s%s" % (self.resource, filename) - delete = webdavlib.WebDAVDELETE(url) - client.execute(delete) - self.assertEquals(delete.response["status"], exp_status, - "%s: card deletion:" - " expected status code '%d' (received '%d')" - % (filename, exp_status, delete.response["status"])) - - def _testCreate(self, rights): - if rights.has_key("c") and rights["c"]: - exp_code = 201 - else: - exp_code = 403 - self._putCard(self.subscriber_client, "new.vcf", exp_code) - - def _testView(self, rights): - if ((rights.has_key("v") and rights["v"]) - or (rights.has_key("e") and rights["e"])): - exp_code = 200 - else: - exp_code = 403 - self._getCard(self.subscriber_client, "old.vcf", exp_code) - - def _testEdit(self, rights): - if rights.has_key("e") and rights["e"]: - exp_code = 204 - else: - exp_code = 403 - self._putCard(self.subscriber_client, "old.vcf", exp_code, "old-modified.vcf") - - def _testDelete(self, rights): - if rights.has_key("d") and rights["d"]: - exp_code = 204 - else: - exp_code = 403 - self._deleteCard(self.subscriber_client, "old.vcf", exp_code) - -class DAVPublicAccessTest(unittest.TestCase): - def setUp(self): - self.client = webdavlib.WebDAVClient(hostname, port) - self.anon_client = webdavlib.WebDAVClient(hostname, port) - self.dav_utility = utilities.TestUtility(self, self.client) - - def testPublicAccess(self): - resource = '/SOGo/so/public' - options = webdavlib.HTTPOPTIONS(resource) - self.anon_client.execute(options) - self.assertEquals(options.response["status"], 404, - "/SOGo/so/public is unexpectedly available") - - resource = '/SOGo/public' - options = webdavlib.HTTPOPTIONS(resource) - self.anon_client.execute(options) - self.assertEquals(options.response["status"], 404, - "/SOGo/public is unexpectedly available") - - resource = '/SOGo/dav/%s' % username - options = webdavlib.HTTPOPTIONS(resource) - self.anon_client.execute(options) - self.assertEquals(options.response["status"], 401, - "Non-public resources should request authentication") - - resource = '/SOGo/dav/public' - options = webdavlib.HTTPOPTIONS(resource) - self.anon_client.execute(options) - self.assertNotEquals(options.response["status"], 401, - "Non-public resources must NOT request authentication") - self.assertEquals(options.response["status"], 200, - "/SOGo/dav/public is not available, check user defaults") - - -class DAVCalendarPublicAclTest(unittest.TestCase): - def setUp(self): - self.createdRsrc = None - self.superuser_client = webdavlib.WebDAVClient(hostname, port, - superuser, superuser_password) - self.client = webdavlib.WebDAVClient(hostname, port, - username, password) - self.subscriber_client = webdavlib.WebDAVClient(hostname, port, - subscriber_username, - subscriber_password) - self.anon_client = webdavlib.WebDAVClient(hostname, port) - - def tearDown(self): - if self.createdRsrc is not None: - delete = webdavlib.WebDAVDELETE(self.createdRsrc) - self.superuser_client.execute(delete) - - def testCollectionAccessNormalUser(self): - """normal user access to (non-)shared resource from su""" - - # 1. all rights removed - parentColl = '/SOGo/dav/%s/Calendar/' % username - self.createdRsrc = '%stest-dav-acl/' % parentColl - for rsrc in [ 'personal', 'test-dav-acl' ]: - resource = '%s%s/' % (parentColl, rsrc) - mkcol = webdavlib.WebDAVMKCOL(resource) - self.client.execute(mkcol) - acl_utility = utilities.TestCalendarACLUtility(self, - self.client, - resource) - acl_utility.setupRights("anonymous", {}) - acl_utility.setupRights(subscriber_username, {}) - acl_utility.setupRights("", {}) - - propfind = webdavlib.WebDAVPROPFIND(parentColl, [ "displayname" ], 1) - self.subscriber_client.execute(propfind) - hrefs = propfind.response["document"] \ - .findall("{DAV:}response/{DAV:}href") - - self.assertEquals(len(hrefs), 1, - "expected 1 href in response instead of %d" % len(hrefs)) - self.assertEquals(hrefs[0].text, parentColl, - "the href must be the 'Calendar' parent coll.") - - acl_utility = utilities.TestCalendarACLUtility(self, - self.client, - self.createdRsrc) - - # 2. creation right added - acl_utility.setupRights(subscriber_username, { "c": True }) - - self.subscriber_client.execute(propfind) - hrefs = propfind.response["document"].findall("{DAV:}response/{DAV:}href") - self.assertEquals(len(hrefs), 4, - "expected 4 hrefs in response, got %d: %s" - % (len(hrefs), ", ".join([ x.text for x in hrefs ]))) - self.assertEquals(hrefs[0].text, parentColl, - "the first href is not a 'Calendar' parent coll.") - - resourceHrefs = { resource: False, - "%s.xml" % resource[:-1]: False, - "%s.ics" % resource[:-1]: False } - for href in hrefs[1:]: - self.assertTrue(resourceHrefs.has_key(href.text), - "received unexpected href: %s" % href.text) - self.assertFalse(resourceHrefs[href.text], - "href was returned more than once: %s" % href.text) - resourceHrefs[href.text] = True - - acl_utility.setupRights(subscriber_username) - - # 3. creation right added for "default user" - # subscriber_username expected to have access, but not "anonymous" - acl_utility.setupRights("", { "c": True }) - - self.subscriber_client.execute(propfind) - hrefs = propfind.response["document"] \ - .findall("{DAV:}response/{DAV:}href") - - self.assertEquals(len(hrefs), 4, - "expected 4 hrefs in response, got %d: %s" - % (len(hrefs), ", ".join([ x.text for x in hrefs ]))) - self.assertEquals(hrefs[0].text, parentColl, - "the first href is not a 'Calendar' parent coll.") - resourceHrefs = { resource: False, - "%s.xml" % resource[:-1]: False, - "%s.ics" % resource[:-1]: False } - for href in hrefs[1:]: - self.assertTrue(resourceHrefs.has_key(href.text), - "received unexpected href: %s" % href.text) - self.assertFalse(resourceHrefs[href.text], - "href was returned more than once: %s" % href.text) - resourceHrefs[href.text] = True - - anonParentColl = '/SOGo/dav/public/%s/Calendar/' % username - anon_propfind = webdavlib.WebDAVPROPFIND(anonParentColl, - [ "displayname" ], 1) - - self.anon_client.execute(anon_propfind) - hrefs = anon_propfind.response["document"] \ - .findall("{DAV:}response/{DAV:}href") - self.assertEquals(len(hrefs), 1, "expected only 1 href in response") - self.assertEquals(hrefs[0].text, anonParentColl, - "the first href is not a 'Calendar' parent coll.") - - acl_utility.setupRights("", {}) - - # 4. creation right added for "anonymous" - # "anonymous" expected to have access, but not subscriber_username - acl_utility.setupRights("anonymous", { "c": True }) - - self.anon_client.execute(anon_propfind) - hrefs = anon_propfind.response["document"] \ - .findall("{DAV:}response/{DAV:}href") - - - self.assertEquals(len(hrefs), 4, - "expected 4 hrefs in response, got %d: %s" - % (len(hrefs), ", ".join([ x.text for x in hrefs ]))) - self.assertEquals(hrefs[0].text, anonParentColl, - "the first href is not a 'Calendar' parent coll.") - anonResource = '%stest-dav-acl/' % anonParentColl - resourceHrefs = { anonResource: False, - "%s.xml" % anonResource[:-1]: False, - "%s.ics" % anonResource[:-1]: False } - for href in hrefs[1:]: - self.assertTrue(resourceHrefs.has_key(href.text), - "received unexpected href: %s" % href.text) - self.assertFalse(resourceHrefs[href.text], - "href was returned more than once: %s" % href.text) - resourceHrefs[href.text] = True - - self.subscriber_client.execute(propfind) - hrefs = propfind.response["document"] \ - .findall("{DAV:}response/{DAV:}href") - self.assertEquals(len(hrefs), 1, "expected only 1 href in response") - self.assertEquals(hrefs[0].text, parentColl, - "the first href is not a 'Calendar' parent coll.") - - def testCollectionAccessSuperUser(self): - # super user accessing (non-)shared res from nu - - parentColl = '/SOGo/dav/%s/Calendar/' % subscriber_username - self.createdRsrc = '%stest-dav-acl/' % parentColl - for rsrc in [ 'personal', 'test-dav-acl' ]: - resource = '%s%s/' % (parentColl, rsrc) - mkcol = webdavlib.WebDAVMKCOL(resource) - self.superuser_client.execute(mkcol) - acl_utility = utilities.TestCalendarACLUtility(self, - self.subscriber_client, - resource) - acl_utility.setupRights(username, {}) - - propfind = webdavlib.WebDAVPROPFIND(parentColl, [ "displayname" ], 1) - self.subscriber_client.execute(propfind) - hrefs = [x.text \ - for x in propfind.response["document"] \ - .findall("{DAV:}response/{DAV:}href")] - self.assertTrue(len(hrefs) > 2, - "expected at least 3 hrefs in response") - self.assertEquals(hrefs[0], parentColl, - "the href must be the 'Calendar' parent coll.") - for rsrc in [ 'personal', 'test-dav-acl' ]: - resource = '%s%s/' % (parentColl, rsrc) - self.assertTrue(hrefs.index(resource) > -1, - "resource '%s' not returned" % resource) - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-default-classification.py b/Tests/Integration/test-default-classification.py deleted file mode 100755 index 8994b6aff..000000000 --- a/Tests/Integration/test-default-classification.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/python - -import sogotests -import unittest -import webdavlib - -from config import * - -class HTTPDefaultClassificationTest(unittest.TestCase): - def _setClassification(self, user, component, classification = ""): - resource = '/SOGo/dav/%s/Calendar/' % user - props = { "{urn:inverse:params:xml:ns:inverse-dav}%s-default-classification" % component: classification } - proppatch = webdavlib.WebDAVPROPPATCH(resource, props) - client = webdavlib.WebDAVClient(hostname, port, username, password) - client.execute(proppatch) - - return (proppatch.response["status"] == 207); - - def _getClassification(self, user, component): - resource = '/SOGo/dav/%s/Calendar/' % user - property_name = "{urn:inverse:params:xml:ns:inverse-dav}%s-default-classification" % component - propfind = webdavlib.WebDAVPROPFIND(resource, [ property_name ], "0") - client = webdavlib.WebDAVClient(hostname, port, username, password) - client.execute(propfind) - classification = None - propstat_nodes = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat") - for propstat_node in propstat_nodes: - status_nodes = propstat_node.findall("{DAV:}status") - if status_nodes[0].text.lower() == "http/1.1 200 ok": - property_nodes = propstat_node.findall("{DAV:}prop/%s" % property_name) - if len(property_nodes) > 0: - classification = property_nodes[0].text - - return classification - - def test(self): - self.assertFalse(self._setClassification(username, "123456", "PUBLIC"), - "expected failure when setting a classification with an invalid property") - self.assertFalse(self._setClassification(username, "events", ""), - "expected failure when setting an empty classification") - self.assertFalse(self._setClassification(username, "events", "pouet"), - "expected failure when setting an invalid classification") - for component in [ "events", "tasks" ]: - for classification in [ "PUBLIC", "PRIVATE", "CONFIDENTIAL" ]: - self.assertTrue(self._setClassification(username, component, classification), - "error when setting classification to '%s'" % classification) - fetched_class = self._getClassification(username, component) - self.assertTrue(classification == fetched_class, - "set and fetched classifications do not match (%s != %s)" % (classification, fetched_class)) - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-ical.py b/Tests/Integration/test-ical.py deleted file mode 100755 index 6f128de7c..000000000 --- a/Tests/Integration/test-ical.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/python - -# FIXME: we should avoid using superuser if possible - -from config import hostname, port, username, password, subscriber_username, \ - superuser, superuser_password - -import unittest -import sogotests -import utilities -import webdavlib - -class iCalTest(unittest.TestCase): - def testPrincipalCollectionSet(self): - """principal-collection-set: 'DAV' header must be returned with iCal 4""" - client = webdavlib.WebDAVClient(hostname, port, username, password) - resource = '/SOGo/dav/%s/' % username - - # NOT iCal4 - propfind = webdavlib.WebDAVPROPFIND(resource, - ["{DAV:}principal-collection-set"], - 0) - client.execute(propfind) - self.assertEquals(propfind.response["status"], 207) - headers = propfind.response["headers"] - self.assertFalse(headers.has_key("dav"), - "DAV header must not be returned when user-agent is NOT iCal 4") - - # iCal4 - propfind = webdavlib.WebDAVPROPFIND(resource, - ["{DAV:}principal-collection-set"], - 0) - client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)" - client.execute(propfind) - self.assertEquals(propfind.response["status"], 207) - headers = propfind.response["headers"] - self.assertTrue(headers.has_key("dav"), - "DAV header must be returned when user-agent is iCal 4") - - expectedDAVClasses = ["1", "2", "access-control", "calendar-access", - "calendar-schedule", "calendar-auto-schedule", - "calendar-proxy"] - davClasses = [x.strip() for x in headers["dav"].split(",")] - for davClass in expectedDAVClasses: - self.assertTrue(davClass in davClasses, - "DAV class '%s' not found" % davClass) - - def _setMemberSet(self, owner, members, perm): - resource = '/SOGo/dav/%s/calendar-proxy-%s/' % (owner, perm) - membersHref = [ { "{DAV:}href": '/SOGo/dav/%s/' % x } - for x in members ] - props = { "{DAV:}group-member-set": membersHref } - proppatch = webdavlib.WebDAVPROPPATCH(resource, props) - client = webdavlib.WebDAVClient(hostname, port, superuser, superuser_password) - client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)" - client.execute(proppatch) - self.assertEquals(proppatch.response["status"], 207, - "failure (%s) setting '%s' permission for '%s' on %s's calendars" - % (proppatch.response["status"], perm, - "', '".join(members), owner)) - - def _getMembership(self, user): - resource = '/SOGo/dav/%s/' % user - propfind = webdavlib.WebDAVPROPFIND(resource, - ["{DAV:}group-membership"], 0) - client = webdavlib.WebDAVClient(hostname, port, superuser, superuser_password) - client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)" - client.execute(propfind) - - hrefs = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}group-membership/{DAV:}href") - members = [x.text for x in hrefs] - - return members - - def _getProxyFor(self, user, perm): - resource = '/SOGo/dav/%s/' % user - prop = "{http://calendarserver.org/ns/}calendar-proxy-%s-for" % perm - propfind = webdavlib.WebDAVPROPFIND(resource, [prop], 0) - client = webdavlib.WebDAVClient(hostname, port, superuser, superuser_password) - client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)" - client.execute(propfind) - - hrefs = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat/{DAV:}prop/{http://calendarserver.org/ns/}calendar-proxy-%s-for/{DAV:}href" - % perm) - members = [x.text[len("/SOGo/dav/"):-1] for x in hrefs] - - return members - - def testCalendarProxy(self): - """calendar-proxy as used from iCal""" - self._setMemberSet(username, [], "read") - self._setMemberSet(username, [], "write") - self._setMemberSet(subscriber_username, [], "read") - self._setMemberSet(subscriber_username, [], "write") - self.assertEquals([], self._getMembership(username), - "'%s' must have no membership" - % username) - self.assertEquals([], self._getMembership(subscriber_username), - "'%s' must have no membership" - % subscriber_username) - self.assertEquals([], self._getProxyFor(username, "read"), - "'%s' must not be a proxy for anyone" % username) - self.assertEquals([], self._getProxyFor(username, "write"), - "'%s' must not be a proxy for anyone" % username) - self.assertEquals([], self._getProxyFor(subscriber_username, "read"), - "'%s' must not be a proxy for anyone" % subscriber_username) - self.assertEquals([], self._getProxyFor(subscriber_username, "write"), - "'%s' must not be a proxy for anyone" % subscriber_username) - - for perm in ("read", "write"): - for users in ((username, subscriber_username), - (subscriber_username, username)): - self._setMemberSet(users[0], [users[1]], perm) - membership = self._getMembership(users[1]) - self.assertEquals(['/SOGo/dav/%s/calendar-proxy-%s/' - % (users[0], perm)], - membership, - "'%s' must have %s access to %s's calendars" - % (users[1], perm, users[0])) - proxyFor = self._getProxyFor(users[1], perm) - self.assertEquals([users[0]], proxyFor, - "'%s' expected to be %s proxy for %s: %s" - % (users[1], perm, users[0], proxyFor)) - - def _testMapping(self, client, perm, resource, rights): - dav_utility = utilities.TestCalendarACLUtility(self, client, resource) - dav_utility.setupRights(subscriber_username, rights) - - membership = self._getMembership(subscriber_username) - self.assertEquals(['/SOGo/dav/%s/calendar-proxy-%s/' - % (username, perm)], - membership, - "'%s' must have %s access to %s's calendars:\n%s" - % (subscriber_username, perm, username, membership)) - proxyFor = self._getProxyFor(subscriber_username, perm) - self.assertEquals([username], proxyFor, - "'%s' expected to be %s proxy for %s: %s" - % (subscriber_username, perm, username, proxyFor)) - - def testCalendarProxy2(self): - """calendar-proxy as used from SOGo""" - client = webdavlib.WebDAVClient(hostname, port, superuser, superuser_password) - client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)" - personal_resource = "/SOGo/dav/%s/Calendar/personal/" % username - dav_utility = utilities.TestCalendarACLUtility(self, - client, - personal_resource) - dav_utility.setupRights(subscriber_username, {}) - dav_utility.subscribe([subscriber_username]) - - other_resource = ("/SOGo/dav/%s/Calendar/test-calendar-proxy2/" - % username) - delete = webdavlib.WebDAVDELETE(other_resource) - client.execute(delete) - mkcol = webdavlib.WebDAVMKCOL(other_resource) - client.execute(mkcol) - dav_utility = utilities.TestCalendarACLUtility(self, - client, - other_resource) - dav_utility.setupRights(subscriber_username, {}) - dav_utility.subscribe([subscriber_username]) - - ## we test the rights mapping - # write: write on 'personal', none on 'test-calendar-proxy2' - self._testMapping(client, "write", personal_resource, - { "c": True, "d": False, "pu": "v" }) - self._testMapping(client, "write", personal_resource, - { "c": False, "d": True, "pu": "v" }) - self._testMapping(client, "write", personal_resource, - { "c": False, "d": False, "pu": "m" }) - self._testMapping(client, "write", personal_resource, - { "c": False, "d": False, "pu": "r" }) - - # read: read on 'personal', none on 'test-calendar-proxy2' - self._testMapping(client, "read", personal_resource, - { "c": False, "d": False, "pu": "d" }) - self._testMapping(client, "read", personal_resource, - { "c": False, "d": False, "pu": "v" }) - - # write: read on 'personal', write on 'test-calendar-proxy2' - self._testMapping(client, "write", other_resource, - { "c": False, "d": False, "pu": "r" }) - - ## we test the unsubscription - # unsubscribed from personal, subscribed to 'test-calendar-proxy2' - dav_utility = utilities.TestCalendarACLUtility(self, client, - personal_resource) - dav_utility.unsubscribe([subscriber_username]) - membership = self._getMembership(subscriber_username) - self.assertEquals(['/SOGo/dav/%s/calendar-proxy-write/' % username], - membership, - "'%s' must have write access to %s's calendars" - % (subscriber_username, username)) - # unsubscribed from personal, unsubscribed from 'test-calendar-proxy2' - dav_utility = utilities.TestCalendarACLUtility(self, client, - other_resource) - dav_utility.unsubscribe([subscriber_username]) - membership = self._getMembership(subscriber_username) - self.assertEquals([], - membership, - "'%s' must have no access to %s's calendars" - % (subscriber_username, username)) - - delete = webdavlib.WebDAVDELETE(other_resource) - client.execute(delete) - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-maildav.py b/Tests/Integration/test-maildav.py deleted file mode 100755 index abdb40426..000000000 --- a/Tests/Integration/test-maildav.py +++ /dev/null @@ -1,677 +0,0 @@ -#!/usr/bin/python - -from config import hostname, port, username, password, mailserver, subscriber_username, subscriber_password - -import sys -import sogotests -import unittest -import webdavlib -import time - -# TODO -# add test with multiple sort criterias - -def fetchUserEmail(login): - client = webdavlib.WebDAVClient(hostname, port, - username, password) - resource = '/SOGo/dav/%s/' % login - propfind = webdavlib.WebDAVPROPFIND(resource, - ["{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"], - 0) - client.execute(propfind) - nodes = propfind.xpath_evaluate('{DAV:}response/{DAV:}propstat/{DAV:}prop/C:calendar-user-address-set/{DAV:}href', - None) - - return nodes[0].childNodes[0].nodeValue - -message1 = """Return-Path: -Received: from cyril.dev (localhost [127.0.0.1]) - by cyril.dev (Cyrus v2.3.8-Debian-2.3.8-1) with LMTPA; - Tue, 17 Dec 2009 07:42:16 -0400 -Received: from aloha.dev (localhost [127.0.0.1]) - by aloha.dev (Cyrus v2.3.8-Debian-2.3.8-1) with LMTPA; - Tue, 29 Sep 2009 07:42:16 -0400 -Message-ID: <4AC1F29sept6.5060801@cyril.dev> -Date: Mon, 28 Sep 2009 07:42:14 -0400 -From: Cyril -User-Agent: Thunderbird 2.0.0.22 (Macintosh/20090605) -References: <4AC3BF1B.3010806@inverse.ca> -MIME-Version: 1.0 -To: message1to@cyril.dev -CC: 2message1cc@cyril.dev, user10@cyril.dev -Subject: message1subject -Content-Type: text/plain; charset=us-ascii; format=flowed -Content-Transfer-Encoding: 7bit -Reply-To: support@inverse.ca - -Hello Jacques, - -Can you read me? - --- -Cyril -""" - -message2 = """Return-Path: -Received: from cyril.dev (localhost [127.0.0.1]) - by cyril.dev (Cyrus v2.3.8-Debian-2.3.8-1) with LMTPA; - Tue, 09 Dec 2009 07:42:16 -0400 -Message-ID: <410sepAC1F296.5060801a@cyril.dev> -Date: Tue, 10 Sep 2009 07:42:14 -0400 -User-Agent: Thunderbird 2.0.0.22 (Macintosh/20090605) -MIME-Version: 1.0 -From: Cyril -To: message2to@cyril.dev -CC: 3message2cc@cyril.dev -Subject: message2subject -Content-Type: text/plain; charset=us-ascii; format=flowed -Content-Transfer-Encoding: 7bit -Reply-To: support@inverse.ca - -Hello Jacques, - -Can you read me? - -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff -Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff --- -Cyril -""" - -message3 = """Return-Path: -Received: from cyril.dev (localhost [127.0.0.1]) - by cyril.dev (Cyrus v2.3.8-Debian-2.3.8-1) with LMTPA; - Tue, 15 Dec 2009 07:42:16 -0400 -Message-ID: <4AC1aF2dec96.5060801a@cyril.dev> -Date: Tue, 10 Dec 2009 07:42:14 -0400 -User-Agent: Thunderbird 2.0.0.22 (Macintosh/20090605) -MIME-Version: 1.0 -From: Cyril -To: message3to@cyril.dev -CC: 1message3cc@cyril.dev -Subject: Hallo -Content-Type: text/plain; charset=us-ascii; format=flowed -Content-Transfer-Encoding: 7bit -Reply-To: support@inverse.ca - -Hello Jacques, - -Can you read me? - -This message is just a bit larger than message1 but smaller than message2 --- -Cyril -""" -message1_received = """Received: from cyril.dev (localhost [127.0.0.1]) - by cyril.dev (Cyrus v2.3.8-Debian-2.3.8-1) with LMTPA; - Tue, 17 Dec 2009 07:42:16 -0400""" - -class DAVMailCollectionTest(): - resource = '/SOGo/dav/%s/Mail/' % username - user_email = None - - def setUp(self): - self.client = webdavlib.WebDAVClient(hostname, port, - username, password) - if self.user_email is None: - self.user_email = fetchUserEmail(username) - if self.user_email.startswith("mailto:"): - self.user_email = self.user_email[7:] - - self.resource = '/SOGo/dav/%s/Mail/%s_A_%s/' \ - % (username, - username.replace("@", "_A_").replace(".", "_D_"), - mailserver.replace(".", "_D_")) - - ## helper methods - def _makeCollection(self, name, status = 201): - url = "%s%s" % (self.resource, name) - mkcol = webdavlib.WebDAVMKCOL(url) - self.client.execute(mkcol) - self.assertEquals(mkcol.response["status"], status, - "failure creating collection" - "(code = %d)" % mkcol.response["status"]) - - def _deleteCollection(self, name, status = 204): - url = "%sfolder%s" % (self.resource, name) - delete = webdavlib.WebDAVDELETE(url) - self.client.execute(delete) - self.assertEquals(delete.response["status"], status, - "failure deleting collection" - "(code = %d)" % delete.response["status"]) - - def _putMessage(self, client, folder, message, - exp_status = 201): - url = "%sfolder%s" % (self.resource, folder) - put = webdavlib.HTTPPUT(url, message) - put.content_type = "message/rfc822" - client.execute(put) - if (exp_status is not None): - self.assertEquals(put.response["status"], exp_status, - "message creation/modification:" - " expected status code '%d' (received '%d')" - % (exp_status, put.response["status"])) - return put.response["headers"]["location"] - - def _testProperty (self, url, property, expected, isDate = 0): - propfind = webdavlib.WebDAVPROPFIND(url, (property, ), 0) - self.client.execute(propfind) - key = property.replace("{urn:schemas:httpmail:}", "a:") - key = key.replace("{urn:schemas:mailheader:}", "a:") - tmp = propfind.xpath_evaluate("{DAV:}response/{DAV:}propstat/{DAV:}prop") - prop = tmp[0].firstChild; - result = None - - if prop: - result = prop._get_firstChild()._get_nodeValue() - #print key, result - - if isDate: - tstruct = time.strptime (result, "%a, %d %b %Y %H:%M:%S %Z") - result = int (time.mktime (tstruct)) - - self.assertEquals(result, expected, - "failure in propfind" - "(%s != %s)" % (result, expected)) - - def testMKCOL(self): - """Folder creation""" - self._deleteCollection("test-dav-mail-%40-abc") - self._deleteCollection("test-dav-mail-@-def") - self._deleteCollection("test-dav-mail-%20-ghi") - - self._makeCollection("test-dav-mail-%40-abc") - self._makeCollection("test-dav-mail-@-def") - self._makeCollection("test-dav-mail-%20-ghi") - self._makeCollection("test-dav-mail-%25-jkl", 500) - - # Test MOVE -# self._makeCollection ("test-dav-mail-movable") -# url = "%sfolder%s" % (self.resource, "test-dav-mail-movable") -# move = webdavlib.WebDAVMOVE (url) -# move.destination = "http://cyril.dev%s%s2" % (self.resource, "test-dav-mail-movable") -# move.host = "cyril.dev" -# self.client.execute (move) -# self.assertEquals(move.response["status"], 204, -# "failure creating collection" -# "(code = %d)" % move.response["status"]) - - def testPUT(self): - """Message creation""" - self._deleteCollection("test-dav-mail") - self._makeCollection("test-dav-mail") - - # message creation on collection url - url = "%s%s" % (self.resource, "foldertest-dav-mail/") - put = webdavlib.HTTPPUT(url, message1) - put.content_type = "message/rfc822" - self.client.execute(put) - self.assertEquals(put.response["status"], 201, - "failure putting message" - "(code = %d)" % put.response["status"]) - - itemLocation = put.response["headers"]["location"] - get = webdavlib.WebDAVGET(itemLocation) - self.client.execute(get) - self.assertEquals(get.response["status"], 200, - "failure getting item" - "(code = %d)" % get.response["status"]) - - # message creation with explicit filename - url = "%s%s" %(self.resource, "foldertest-dav-mail/blabla.eml") - put = webdavlib.HTTPPUT(url, message1) - put.content_type = "message/rfc822" - self.client.execute(put) - self.assertEquals(put.response["status"], 201, - "failure putting message" - "(code = %d)" % put.response["status"]) - - itemLocation = put.response["headers"]["location"] - get = webdavlib.WebDAVGET(itemLocation) - self.client.execute(get) - self.assertEquals(get.response["status"], 200, - "failure getting item" - "(code = %d)" % get.response["status"]) - - self._deleteCollection("test-dav-mail") - - def _testFilters(self, filters): - for filter in filters: - self._testFilter(filter) - - def _testFilter(self, filter): - expected_hrefs = {} - expected_count = len(filter[1]) - for href in filter[1]: - expected_hrefs[href] = True - - received_count = 0 - url = "%sfolder%s" % (self.resource, "test-dav-mail") - query = webdavlib.MailDAVMailQuery(url, ["displayname"], filter[0]) - self.client.execute(query) - self.assertEquals(query.response["status"], 207, - "filter %s:\n\tunexpected status: %d" - % (filter[0], query.response["status"])) - response_nodes = query.xpath_evaluate("{DAV:}response") - for response_node in response_nodes: - href_node = query.xpath_evaluate("{DAV:}href", response_node)[0] - href = href_node.childNodes[0].nodeValue - received_count = received_count + 1 - self.assertTrue(expected_hrefs.has_key(href), - "filter %s:\n\tunexpected href: %s" % (filter[0], href)) - - self.assertEquals(len(filter[1]), received_count, - "filter %s:\n\tunexpected amount of refs: %d" - % (filter[0], received_count)) - - def _testSort(self, sortOrder, ascending = True): - expected_hrefs = sortOrder[1] - expected_count = len(expected_hrefs) - - received_count = 0 - url = "%sfolder%s" % (self.resource, "test-dav-mail") - query = webdavlib.MailDAVMailQuery(url, ["displayname"], - None, sortOrder[0], ascending) - self.client.execute(query) - self.assertEquals(query.response["status"], 207, - "sortOrder %s:\n\tunexpected status: %d" - % (sortOrder[0], query.response["status"])) - response_nodes = query.response["document"].findall("{DAV:}response") - for response_node in response_nodes: - href_node = response_node.find("{DAV:}href") - href = href_node.text - self.assertEquals(expected_hrefs[received_count], href, - "sortOrder %s:\n\tunexpected href: %s (expecting: %s)" - % (sortOrder[0], href, - expected_hrefs[received_count])) - received_count = received_count + 1 - - self.assertEquals(expected_count, received_count, - "sortOrder %s:\n\tunexpected amount of refs: %d" - % (sortOrder[0], received_count)) - - def testREPORTMailQueryFilters(self): - """mail-query filters""" - self._deleteCollection("test-dav-mail") - self._makeCollection("test-dav-mail") - - msg1Loc = self._putMessage(self.client, "test-dav-mail", message1) - parsed = webdavlib.HTTPUnparsedURL(msg1Loc) - msg1Path = parsed.path - msg2Loc = self._putMessage(self.client, "test-dav-mail", message2) - parsed = webdavlib.HTTPUnparsedURL(msg2Loc) - msg2Path = parsed.path - msg3Loc = self._putMessage(self.client, "test-dav-mail", message3) - parsed = webdavlib.HTTPUnparsedURL(msg3Loc) - msg3Path = parsed.path - - properties = ["{DAV:}displayname"] - - ## 1. test filter: receive-date - # SINCE, BEFORE, ON - # q = MailDAVMailQuery(self.resource, properties, filters = None) - - filters = (({ "receive-date": { "from": "20091201T000000Z", - "to": "20091208T000000Z" } }, - []), - ({ "receive-date": { "from": "20091208T000000Z", - "to": "20091213T134300Z" } }, - [ msg2Loc ]), - ({ "receive-date": { "from": "20091208T000000Z", - "to": "20091216T134300Z" } }, - [ msg2Loc, msg3Loc ]), - ({ "receive-date": { "from": "20091216T000000Z", - "to": "20091220T134300Z" } }, - [ msg1Loc ]), - ({ "receive-date": { "from": "20091220T000000Z", - "to": "20091229T134300Z" } }, - [])) - # receive-date seems to be considered the same as date by imapd - print "Warning, receive-date test disabled" - #self._testFilters(filters) - - ## 1. test filter: date - # SENTSINCE, SENTBEFORE, SENTON - - filters = (({ "date": { "from": "20090101T000000Z", - "to": "20090201T000000Z" } }, - []), - ({ "date": { "from": "20090912T000000Z", - "to": "20090929T134300Z" } }, - [ msg1Loc ]), - ({ "date": { "from": "20090929T134300Z", - "to": "20091209T000000Z" } }, - []), - ({ "date": { "from": "20090901T134300Z", - "to": "20091209T000000Z" } }, - [ msg1Loc, msg2Loc ]), - ({ "date": { "from": "20091201T000000Z", - "to": "20091211T000000Z" } }, - [ msg3Loc ]), - ({ "date": { "from": "20091211T000000Z", - "to": "20101211T000000Z" } }, - []), - ({ "date": { "from": "20090101T000000Z", - "to": "20100101T000000Z" } }, - [ msg1Loc, msg2Loc, msg3Loc ])) - self._testFilters(filters) - - ## 1. test filter: sequence - # x:y - filters = (({ "sequence": { "from": "1" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "sequence": { "from": "5" }}, - []), - ({ "sequence": { "to": "5" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "sequence": { "from": "1", - "to": "2" }}, - [ msg1Loc, msg2Loc ])) - # Sequence not yet implemented - print "Warning, sequence test disabled" - #self._testFilters(filters) - - ## 1. test filter: uid - # UID - filters = (({ "uid": { "from": "1" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - # disabled because we get 3 - #({ "uid": { "from": "5" }}, - # []), - ({ "uid": { "to": "5" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "uid": { "from": "1", - "to": "2" }}, - [ msg1Loc, msg2Loc ])) - print "Warning, one of the uid tests is disabled" - self._testFilters(filters) - - ## 1. test filter: from - # FROM - filters = (({ "from": { "match": "message" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "from": { "match": "Cyril" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "from": { "match": "cyril.dev" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "from": { "match": "message1from" }}, - [ msg1Loc ]), - ({ "from": { "match": "message2from" }}, - [ msg2Loc ]), - ({ "from": { "match": "message3from" }}, - [ msg3Loc ])) - self._testFilters(filters) - - ## 1. test filter: to - # TO - filters = (({ "to": { "match": "message" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "to": { "match": "Cyril" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "to": { "match": "cyril.dev" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "to": { "match": "message1to" }}, - [ msg1Loc ]), - ({ "to": { "match": "message2to" }}, - [ msg2Loc ]), - ({ "to": { "match": "message3to" }}, - [ msg3Loc ])) - self._testFilters(filters) - - ## 1. test filter: cc - # CC - filters = (({ "cc": { "match": "message" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "cc": { "match": "Cyril" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "cc": { "match": "cyril.dev" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "cc": { "match": "message1cc" }}, - [ msg1Loc ]), - ({ "cc": { "match": "message2cc" }}, - [ msg2Loc ]), - ({ "cc": { "match": "message3cc" }}, - [ msg3Loc ])) - self._testFilters(filters) - - ## 1. test filter: bcc - # BCC - ## TODO - - ## 1. test filter: body - # BODY - filters = (({ "body": { "match": "Hello" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "body": { "match": "Stuff" }}, - [ msg2Loc ]), - ({ "body": { "match": "DOESNOT MATCH" }}, - [])) - self._testFilters(filters) - - ## 1. test filter: size - # LARGER, SMALLER - #1 848 - #2 4308 - #3 699 - filters = (({ "size": { "min": "300", - "max": "300" }}, - []), - ({ "size": { "min": "800", - "max": "800" }}, - []), - ({ "size": { "min": "5000", - "max": "5000" }}, - []), - ({ "size": { "min": "838", - "max": "838" }}, - [ msg1Loc ]), - ({ "size": { "min": "699", - "max": "4308" }}, - [ msg1Loc, msg2Loc, msg3Loc ]), - ({ "size": { "min": "700", - "max": "4308" }}, - [ msg1Loc, msg2Loc ]), - ({ "size": { "min": "698", - "max": "848" }}, - [ msg1Loc, msg3Loc ]), - ({ "size": { "min": "300", - "max": "5000" }, - "size": { "min": "840", - "max": "850", - "not": "true" }}, - [ msg2Loc, msg3Loc ])) - - print "message flags are not handled yet" - ## 1. test filter: answered - # ANSWERED, UNANSWERED - ## 1. test filter: draft - # DRAFT - ## 1. test filter: flagged - # FLAGGED - ## 1. test filter: recent - # RECENT - ## 1. test filter: seen - # SEEN - ## 1. test filter: deleted - # DELETED - ## 1. test filter: keywords - # KEYWORD x - - ## 1. test filter: multiple combinations - filters = (({ "body": { "match": "Hello" }, - "cc": { "match": "message1cc" }}, - [ msg1Loc ]), - ({ "to": { "match": "message" }, - "uid": { "from": "1", - "to": "2" }}, - [ msg1Loc, msg2Loc ]), - ({ "to": { "match": "message" }, - "uid": { "from": "1", - "to": "2" }, - "cc": { "match": "message3cc" }}, - [])) - self._testFilters(filters) - - self._deleteCollection("test-dav-mail") - - def testREPORTMailQuerySort(self): - """mail-query sort""" - self._deleteCollection("test-dav-mail") - self._makeCollection("test-dav-mail") - - msg1Loc = self._putMessage(self.client, "test-dav-mail", message1) - parsed = webdavlib.HTTPUnparsedURL(msg1Loc) - msg1Path = parsed.path - msg2Loc = self._putMessage(self.client, "test-dav-mail", message2) - parsed = webdavlib.HTTPUnparsedURL(msg2Loc) - msg2Path = parsed.path - msg3Loc = self._putMessage(self.client, "test-dav-mail", message3) - parsed = webdavlib.HTTPUnparsedURL(msg3Loc) - msg3Path = parsed.path - - # 1. test sort: (receive-date) ARRIVAL - self._testSort(([ "{urn:schemas:mailheader:}received" ], - [ msg1Loc, msg2Loc, msg3Loc ])) - - # 1. test sort: (date) DATE - self._testSort(([ "{urn:schemas:mailheader:}date" ], - [ msg2Loc, msg1Loc, msg3Loc ])) - - # 1. test sort: FROM - self._testSort(([ "{urn:schemas:mailheader:}from" ], - [ msg1Loc, msg2Loc, msg3Loc ])) - - # 1. test sort: TO - self._testSort(([ "{urn:schemas:mailheader:}to" ], - [ msg1Loc, msg2Loc, msg3Loc ])) - - # 1. test sort: CC - self._testSort(([ "{urn:schemas:mailheader:}cc" ], - [ msg3Loc, msg1Loc, msg2Loc ])) - - # 1. test sort: SUBJECT - self._testSort(([ "{DAV:}displayname" ], - [ msg3Loc, msg1Loc, msg2Loc ])) - self._testSort(([ "{urn:schemas:mailheader:}subject" ], - [ msg3Loc, msg1Loc, msg2Loc ])) - - # 1. test sort: SIZE - self._testSort(([ "{DAV:}getcontentlength" ], - [ msg3Loc, msg1Loc, msg2Loc ])) - - # 1. test sort: REVERSE CC - self._testSort(([ "{urn:schemas:mailheader:}cc" ], - [ msg2Loc, msg1Loc, msg3Loc ]), - False) - - self._deleteCollection("test-dav-mail") - - def testPROPFIND(self): - """Message properties""" - self._deleteCollection ("test-dav-mail") - self._makeCollection ("test-dav-mail") - - url = "%s%s" % (self.resource, "foldertest-dav-mail/") - put = webdavlib.HTTPPUT (url, message1) - put.content_type = "message/rfc822" - self.client.execute (put) - self.assertEquals(put.response["status"], 201, - "failure putting message" - "(code = %d)" % put.response["status"]) - - itemLocation = put.response["headers"]["location"] - tests = (("{urn:schemas:httpmail:}date", 1254156134, 1), - ("{urn:schemas:httpmail:}hasattachment", "0", 0), - ("{urn:schemas:httpmail:}read", "0", 0), - ("{urn:schemas:httpmail:}textdescription", - "" % message1, 0), - ("{urn:schemas:httpmail:}unreadcount", None, 0), - ("{urn:schemas:mailheader:}cc", - "2message1cc@cyril.dev, user10@cyril.dev", 0), - ("{urn:schemas:mailheader:}date", - "Mon, 28 Sep 2009 11:42:14 GMT", 0), - ("{urn:schemas:mailheader:}from", - "Cyril ", 0), - ("{urn:schemas:mailheader:}in-reply-to", None, 0), - ("{urn:schemas:mailheader:}message-id", - "<4AC1F29sept6.5060801@cyril.dev>", 0), - ("{urn:schemas:mailheader:}received", message1_received, 0), - ("{urn:schemas:mailheader:}references", - "<4AC3BF1B.3010806@inverse.ca>", 0), - ("{urn:schemas:mailheader:}subject", "message1subject", 0), - ("{urn:schemas:mailheader:}to", "message1to@cyril.dev", 0)) - - for test in tests: - property, expected, isDate = test - self._testProperty(itemLocation, property, expected, isDate) - - self._deleteCollection ("test-dav-mail") - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-preferences.py b/Tests/Integration/test-preferences.py deleted file mode 100644 index a13eb16be..000000000 --- a/Tests/Integration/test-preferences.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/python -from config import hostname, port, username, password, white_listed_attendee - -import preferences -import simplejson -import sogotests -import unittest -import utilities - -class preferencesTest(unittest.TestCase): - - def _setTextPref(self, prefText = None ): - """ set a text preference to a known value """ - self.prefs.set("autoReplyText", prefText) - - # make sure it was set correctly - prefData = self.prefs.get("Vacation") - - self.assertEqual(prefData["autoReplyText"], prefText, - "%s != %s" % (prefData["autoReplyText"], prefText)) - - def setUp(self): - self.prefs = preferences.preferences() - - def tearDown(self): - self.prefs.set("autoReplyText", "") - - def testSetTextPreferences(self): - """ Set/get a text preference - normal characters""" - - self._setTextPref("defaultText") - - def testSetTextPreferencesWeirdChars(self): - """ Set/get a text preference - weird characters - used to crash on 1.3.12""" - prefText = "weird data \ ' \"; ^" - self._setTextPref(prefText) - - def testSetPreventInvitation(self): - """ Set/get the PreventInvitation pref""" - self.prefs.set('PreventInvitations', '0') - notset = self.prefs.get_settings('')['Calendar']['PreventInvitations'] - self.assertEqual(notset, 0) - self.prefs.set('enablePreventInvitations', '0') - isset = self.prefs.get_settings('')['Calendar']['PreventInvitations'] - self.assertEqual(isset, 1) - - def testPreventInvitationsWhiteList(self): - """Add to the PreventInvitations Whitelist""" - self.prefs.set("whiteList", simplejson.dumps(white_listed_attendee)) - whitelist = self.prefs.get_settings('Calendar')['PreventInvitationsWhitelist'] - self.assertEqual(whitelist, white_listed_attendee) - - - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-prevent-invitations.py b/Tests/Integration/test-prevent-invitations.py deleted file mode 100755 index 4493597db..000000000 --- a/Tests/Integration/test-prevent-invitations.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/python -from config import hostname, port, username, password, \ - superuser, superuser_password, \ - attendee1, attendee1_username, \ - attendee1_password, \ - attendee1_delegate, attendee1_delegate_username, \ - attendee1_delegate_password, \ - resource_no_overbook, resource_can_overbook, \ - white_listed_attendee - -import preferences -import simplejson -import sogotests -import unittest -import utilities -import datetime -import dateutil.tz -import vobject -import vobject.base -import vobject.icalendar -import webdavlib -import StringIO - - -class preventInvitationsTest(unittest.TestCase): - def setUp(self): - self.prefs = preferences.preferences(attendee1, attendee1_password) - self.caldav = CalDAVSchedulingTest(self) - - def tearDown(self): - self.prefs.set("autoReplyText", "") - self.prefs.set('PreventInvitations', '0') - self.prefs.set("whiteList", "") - #- Manual Cleanup, not called because classs is not derived from unittest - self.caldav.tearDown() - - def testDontPreventInvitation(self): - """ Set/get the PreventInvitation pref""" - #- First accept the invitation - self.prefs.set('PreventInvitations', '0') - notset = self.prefs.get_settings('')['Calendar']['PreventInvitations'] - self.assertEqual(notset, 0) - self.caldav.AddAttendee() - self.caldav.VerifyEvent() - - def testPreventInvitation(self): - """ Set PreventInvitation and don't accept the Invitation""" - #- Second, enable PreventInviation and refuse it - self.prefs.set('enablePreventInvitations', '0') - isset = self.prefs.get_settings('')['Calendar']['PreventInvitations'] - self.assertEqual(isset, 1) - self.caldav.AddAttendee(409) - self.caldav.VerifyEvent(404) - - def testPreventInvitationWhiteList(self): - """ Set PreventInvitation add to WhiteList and accept the Invitation""" - #- First, add the Organiser to the Attendee's whitelist - self.prefs.set('enablePreventInvitations', '0') - self.prefs.set("whiteList", simplejson.dumps(white_listed_attendee)) - whitelist = self.prefs.get_settings('Calendar')['PreventInvitationsWhitelist'] - self.assertEqual(whitelist, white_listed_attendee) - - #- Second, try again to invite, it should work - self.prefs.set('enablePreventInvitations', '0') - isset = self.prefs.get_settings('')['Calendar']['PreventInvitations'] - self.assertEqual(isset, 1) - self.caldav.AddAttendee() - self.caldav.VerifyEvent() - - -class CalDAVSchedulingTest(object): - def __init__(self, parent_self): - self.test = parent_self # used for utilities - self.setUp() - - def setUp(self): - self.superuser_client = webdavlib.WebDAVClient(hostname, port, - superuser, superuser_password) - self.client = webdavlib.WebDAVClient(hostname, port, - username, password) - self.attendee1_client = webdavlib.WebDAVClient(hostname, port, - attendee1_username, attendee1_password) - self.attendee1_delegate_client = webdavlib.WebDAVClient(hostname, port, - attendee1_delegate_username, attendee1_delegate_password) - - utility = utilities.TestUtility(self.test, self.client) - (self.user_name, self.user_email) = utility.fetchUserInfo(username) - (self.attendee1_name, self.attendee1_email) = utility.fetchUserInfo(attendee1) - (self.attendee1_delegate_name, self.attendee1_delegate_email) = utility.fetchUserInfo(attendee1_delegate) - - self.user_calendar = "/SOGo/dav/%s/Calendar/personal/" % username - self.attendee1_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1 - self.attendee1_delegate_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1_delegate - - # fetch non existing event to let sogo create the calendars in the db - self._getEvent(self.client, "%snonexistent" % self.user_calendar, exp_status=404) - self._getEvent(self.attendee1_client, "%snonexistent" % self.attendee1_calendar, exp_status=404) - self._getEvent(self.attendee1_delegate_client, "%snonexistent" % - self.attendee1_delegate_calendar, exp_status=404) - - # list of ics used by the test. - # tearDown will loop over this and wipe them in all users' calendar - self.ics_list = [] - - - def tearDown(self): - # delete all created events from all users' calendar - for ics in self.ics_list: - self._deleteEvent(self.superuser_client, - "%s%s" % (self.user_calendar, ics), None) - self._deleteEvent(self.superuser_client, - "%s%s" % (self.attendee1_calendar, ics), None) - self._deleteEvent(self.superuser_client, - "%s%s" % (self.attendee1_delegate_calendar, ics), None) - - def _newEvent(self, summary="test event", uid="test", transp=0): - transparency = ("OPAQUE", "TRANSPARENT") - - newCal = vobject.iCalendar() - vevent = newCal.add('vevent') - vevent.add('summary').value = summary - vevent.add('transp').value = transparency[transp] - - now = datetime.datetime.now(dateutil.tz.gettz("America/Montreal")) - startdate = vevent.add('dtstart') - startdate.value = now - enddate = vevent.add('dtend') - enddate.value = now + datetime.timedelta(0, 3600) - vevent.add('uid').value = uid - vevent.add('dtstamp').value = now - vevent.add('last-modified').value = now - vevent.add('created').value = now - vevent.add('class').value = "PUBLIC" - vevent.add('sequence').value = "0" - - return newCal - - def _putEvent(self, client, filename, event, exp_status = 201): - put = webdavlib.HTTPPUT(filename, event.serialize()) - put.content_type = "text/calendar; charset=utf-8" - client.execute(put) - if exp_status is not None: - self.test.assertEquals(put.response["status"], exp_status) - - def _getEvent(self, client, filename, exp_status = 200): - get = webdavlib.HTTPGET(filename) - client.execute(get) - - if exp_status is not None: - self.test.assertEquals(get.response["status"], exp_status) - - if get.response["headers"]["content-type"].startswith("text/calendar"): - stream = StringIO.StringIO(get.response["body"]) - event = vobject.base.readComponents(stream).next() - else: - event = None - - return event - - def _deleteEvent(self, client, filename, exp_status = 204): - delete = webdavlib.WebDAVDELETE(filename) - client.execute(delete) - if exp_status is not None: - self.test.assertEquals(delete.response["status"], exp_status) - - def AddAttendee(self, exp_status=204): - """ add attendee after event creation """ - - # make sure the event doesn't exist - ics_name = "test-add-attendee.ics" - self.ics_list += [ics_name] - - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ics_name), None) - self._deleteEvent(self.attendee1_client, - "%s%s" % (self.attendee1_calendar,ics_name), None) - - # 1. create an event in the organiser's calendar - event = self._newEvent(summary="Test add attendee", uid="Test add attendee") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # 2. add an attendee - event.add("method").value = "REQUEST" - attendee = event.vevent.add('attendee') - attendee.cn_param = self.attendee1_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.attendee1_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event, - exp_status=exp_status) - - #- save event for VerifyEvent - self.event = event - self.ics_name = ics_name - - def VerifyEvent(self, exp_status=200): - # 1. verify that the attendee has the event - attendee_event = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, self.ics_name), exp_status) - - # 2. make sure the received event match the original one - if attendee_event: - self.test.assertEquals(self.event.vevent.uid, attendee_event.vevent.uid) - - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-sieve.py b/Tests/Integration/test-sieve.py deleted file mode 100644 index 44605e364..000000000 --- a/Tests/Integration/test-sieve.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/python -from config import hostname, port, username, password, sieve_port, sieve_server - -import managesieve -import preferences -import sogotests -import unittest -import utilities -import webdavlib - -sieve_simple_vacation="""require ["vacation"];\r\nvacation :days %(days)d :addresses ["%(mailaddr)s"] text:\r\n%(vacation_msg)s\r\n.\r\n;\r\n""" -sieve_vacation_ignoreLists="""require ["vacation"];\r\nif allof ( not exists ["list-help", "list-unsubscribe", "list-subscribe", "list-owner", "list-post", "list-archive", "list-id", "Mailing-List"], not header :comparator "i;ascii-casemap" :is "Precedence" ["list", "bulk", "junk"], not header :comparator "i;ascii-casemap" :matches "To" "Multiple recipients of*" ) { vacation :days %(days)d :addresses ["%(mailaddr)s"] text:\r\n%(vacation_msg)s\r\n.\r\n;\r\n}\r\n""" -sieve_simple_forward="""redirect "%(redirect_mailaddr)s";\r\n""" -sieve_forward_keep="""redirect "%(redirect_mailaddr)s";\r\nkeep;\r\n""" -sieve_simple_filter="""require ["fileinto"];\r\nif anyof (header :contains "subject" "%(subject)s") {\r\n fileinto "%(folderName)s";\r\n}\r\n""" - -class sieveTest(unittest.TestCase): - def _killFilters(self): - filtersKill={} - # kill existing filters - filtersKill["SOGoSieveFilters"]= "[]" - # vacation filters - filtersKill["autoReplyText"] = "" - filtersKill["autoReplyEmailAddresses"] = "" - # forwarding filters - filtersKill["forwardAddress"] = "" - - self.prefs=preferences.preferences() - self.prefs.set(filtersKill) - - def setUp(self): - ret = "" - - self.client = webdavlib.WebDAVClient(hostname, port, - username, password) - utility = utilities.TestUtility(self, self.client) - (self.user_name, self.user_email) = utility.fetchUserInfo(username) - self.user_email = self.user_email.replace("mailto:", "") - - self.ms = managesieve.MANAGESIEVE(sieve_server, sieve_port) - self.assertEqual(self.ms.login("", username, password), "OK", - "Couldn't login") - - self._killFilters() - - def tearDown(self): - self._killFilters() - - def _getSogoSieveScript(self): - sieveFoundsogo=0 - createdSieveScript="" - (ret, sieveScriptList) = self.ms.listscripts() - self.assertEqual(ret, "OK", "Couldn't get sieve script list") - - for (script, isActive) in sieveScriptList: - if (script == "sogo"): - sieveFoundsogo=1 - self.assertEqual(isActive, True, "sogo sieve script is not active!") - (ret, createdSieveScript) = self.ms.getscript(script) - self.assertEqual(ret, "OK", "Couldn't get sogo sieve script") - - self.assertEqual(sieveFoundsogo, 1, "sogo sieve script not found!") - - return createdSieveScript - - def testSieveSimpleVacation(self): - """ enable simple vacation script """ - vacation_msg="vacation test" - daysSelect=3 - - sieveScript = sieve_simple_vacation % { "mailaddr": self.user_email, - "vacation_msg": vacation_msg, - "days": preferences.daysBetweenResponseList[daysSelect], - } - - filterAdd = {"Vacation":"1", - "autoReplyText": vacation_msg, - "daysBetweenResponse": "%d" % daysSelect, - "autoReplyEmailAddresses": self.user_email, - } - - self.prefs.set(filterAdd) - createdSieveScript=self._getSogoSieveScript() - - self.assertEqual(sieveScript, createdSieveScript) - - def testSieveVacationIgnoreLists(self): - """ enable vacation script - ignore lists""" - vacation_msg="vacation test - ignore list" - daysSelect=2 - - sieveScript = sieve_vacation_ignoreLists % { "mailaddr": self.user_email, - "vacation_msg": vacation_msg, - "days": preferences.daysBetweenResponseList[daysSelect], - } - - filterAdd = {"Vacation":"1", - "autoReplyText": vacation_msg, - "daysBetweenResponse": "%d" % daysSelect, - "autoReplyEmailAddresses": self.user_email, - "ignoreLists": "1", - } - - self.prefs.set(filterAdd) - createdSieveScript=self._getSogoSieveScript() - - self.assertEqual(sieveScript, createdSieveScript) - - def testSieveSimpleForward(self): - """ enable simple forwarding """ - redirect_mailaddr="nonexistent@inverse.com" - - sieveScript = sieve_simple_forward % { "redirect_mailaddr": redirect_mailaddr } - - filterAdd = { "Forward": "1", - "forwardAddress": redirect_mailaddr, - } - - self.prefs.set(filterAdd) - createdSieveScript = self._getSogoSieveScript() - self.assertEqual(sieveScript, createdSieveScript) - - def testSieveForwardKeepCopy(self): - """ enable email forwarding - keep a copy """ - redirect_mailaddr="nonexistent@inverse.com" - - sieveScript = sieve_forward_keep % { "redirect_mailaddr": redirect_mailaddr } - - filterAdd = { "Forward": "1", - "forwardAddress": redirect_mailaddr, - "keepCopy": "1", - } - - self.prefs.set(filterAdd) - createdSieveScript = self._getSogoSieveScript() - self.assertEqual(sieveScript, createdSieveScript) - - def testSieveSimpleFilter(self): - """ add simple sieve filter """ - folderName="Sent" - subject=__name__ - - sieveScript=sieve_simple_filter % { "subject": subject, "folderName": folderName } - - filterAdd = { "SOGoSieveFilters": """[{"active": true, "actions": [{"method": "fileinto", "argument": "Sent"}], "rules": [{"operator": "contains", "field": "subject", "value": "%s"}], "match": "any", "name": "%s"}]""" % (subject, folderName) - } - - self.prefs.set(filterAdd) - createdSieveScript = self._getSogoSieveScript() - self.assertEqual(sieveScript, createdSieveScript) - - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-sogo-tool.py b/Tests/Integration/test-sogo-tool.py deleted file mode 100755 index edd902d72..000000000 --- a/Tests/Integration/test-sogo-tool.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/python - -# XXX this script has to be run as root because it su to sogo_user -# in order to use its .GNUstepDefaults prefs -# Would be much better to have another way to specify which Defaults to use - -from config import sogo_user, sogo_tool_path - -import os -import pwd -import shutil -import sogotests -import tempfile -import unittest - -class sogoToolTest(unittest.TestCase): - - def setUp(self): - self.backupdir = tempfile.mkdtemp() - - def tearDown(self): - os.chdir("/") - shutil.rmtree(self.backupdir) - - def testBackupAll(self): - """ sogo-tool backup ALL """ - - (uid, gid) = pwd.getpwnam(sogo_user)[2:4] - - # We need to run as root since there's no way - # of using another user's GNUstepDefaults - self.assertEqual(os.getuid(), 0, "this test must run as root...") - - os.chown(self.backupdir, uid, gid) - cmd = "sudo -u %s bash -c \"(cd %s && %s backup . ALL >/dev/null 2>&1)\"" % (sogo_user, self.backupdir, sogo_tool_path) - #print "sogo-tool cmd to execute %s" % cmd - status = os.system(cmd) - #print "Exit status of os.system(): %d" % status - rc = os.WEXITSTATUS(status) - #self.assertEqual(rc, 0, "sogo-tool failed RC=%d" % rc) - - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-ui-posts.py b/Tests/Integration/test-ui-posts.py deleted file mode 100644 index 11a5477ac..000000000 --- a/Tests/Integration/test-ui-posts.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/python - - -from config import hostname, port, username, password, \ - webCalendarURL - -import simplejson -import sogoLogin -import sogotests -import unittest -import utilities -import webdavlib -import httplib - - -class UIPostsTests(unittest.TestCase): - - def setUp(self): - self.client = webdavlib.WebDAVClient(hostname, port) - self.gcClient = webdavlib.WebDAVClient(hostname, port) - self.cookie = sogoLogin.getAuthCookie(hostname, port, username, password) - - def _urlPostData(self, client, url, data, exp_status=200): - post = webdavlib.HTTPPOST(url, data) - post.content_type = "application/x-www-form-urlencoded" - post.cookie = self.cookie - - client.execute(post) - if (exp_status is not None): - self.assertEquals(post.response["status"], exp_status) - return post.response - - def _urlGet(self, client, url, exp_status=200): - get = webdavlib.HTTPGET(url) - get.cookie = self.cookie - - client.execute(get) - if (exp_status is not None): - self.assertEquals(get.response["status"], exp_status) - return get.response - - def testAddWebCalendar(self): - """ Add Web Calendar """ - - ret=True - data = "url=%s" % webCalendarURL - calendarBaseURL="/SOGo/so/%s/Calendar" % username - addWebCalendarURL = "%s/addWebCalendar" % calendarBaseURL - response = self._urlPostData(self.client, addWebCalendarURL, data) - - respJSON = simplejson.loads(response['body']) - folderID = respJSON['folderID'] - - #sogo1:Calendar/C07-5006F300-1-370E2480 - (_, calID) = folderID.split('/', 1) - self.assertNotEqual(calID, None) - - # reload the cal - calURL = "%s/%s" % (calendarBaseURL, calID) - try: - response = self._urlGet(self.client, "%s/reload" % calURL, exp_status=None) - except httplib.BadStatusLine: - # that's bad, the server probably reset the connection. fake a 502 - response['status'] = 502 - - # cleanup our trash - self._urlPostData(self.gcClient, "%s/delete" % calURL, "", exp_status=None) - - # delayed assert to allow cal deletion on failure - self.assertEqual(response['status'], 200) - - - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-webdav.py b/Tests/Integration/test-webdav.py deleted file mode 100755 index f77e7ce1f..000000000 --- a/Tests/Integration/test-webdav.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/python - -from config import hostname, port, username, password, subscriber_username - -import sogotests -import unittest -import utilities -import webdavlib - -class WebDAVTest(unittest.TestCase): - def __init__(self, arg): - unittest.TestCase.__init__(self, arg) - self.client = webdavlib.WebDAVClient(hostname, port, - username, password) - self.dav_utility = utilities.TestUtility(self, self.client) - - def testPrincipalCollectionSet(self): - """property: 'principal-collection-set' on collection object""" - resource = '/SOGo/dav/%s/' % username - propfind = webdavlib.WebDAVPROPFIND(resource, - ["{DAV:}principal-collection-set"], - 0) - self.client.execute(propfind) - self.assertEquals(propfind.response["status"], 207) - nodes = propfind.response["document"] \ - .findall('{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}principal-collection-set/{DAV:}href') - responseHref = nodes[0].text - if responseHref[0:4] == "http": - self.assertEquals("http://%s/SOGo/dav/" % hostname, responseHref, - "{DAV:}principal-collection-set returned %s instead of 'http../SOGo/dav/'" - % ( responseHref, resource )) - else: - self.assertEquals("/SOGo/dav/", responseHref, - "{DAV:}principal-collection-set returned %s instead of '/SOGo/dav/'" - % responseHref) - - def testPrincipalCollectionSet2(self): - """property: 'principal-collection-set' on non-collection object""" - resource = '/SOGo/dav/%s/freebusy.ifb' % username - propfind = webdavlib.WebDAVPROPFIND(resource, - ["{DAV:}principal-collection-set"], - 0) - self.client.execute(propfind) - self.assertEquals(propfind.response["status"], 207) - node = propfind.response["document"] \ - .find('{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}principal-collection-set/{DAV:}href') - responseHref = node.text - expectedHref = '/SOGo/dav/' - if responseHref[0:4] == "http": - self.assertEquals("http://%s%s" % (hostname, expectedHref), responseHref, - "{DAV:}principal-collection-set returned %s instead of '%s'" - % ( responseHref, expectedHref )) - else: - self.assertEquals(expectedHref, responseHref, - "{DAV:}principal-collection-set returned %s instead of '%s'" - % ( responseHref, expectedHref )) - - def _testPropfindURL(self, resource): - resourceWithSlash = resource[-1] == '/' - propfind = webdavlib.WebDAVPROPFIND(resource, - ["{DAV:}displayname", "{DAV:}resourcetype"], - 1) - self.client.execute(propfind) - self.assertEquals(propfind.response["status"], 207) - - nodes = propfind.response["document"].findall('{DAV:}response') - for node in nodes: - responseHref = node.find('{DAV:}href').text - hasSlash = responseHref[-1] == '/' - resourcetype = node.find('{DAV:}propstat/{DAV:}prop/{DAV:}resourcetype') - isCollection = len(resourcetype.getchildren()) > 0 - if isCollection: - self.assertEquals(hasSlash, resourceWithSlash, - "failure with href '%s' while querying '%s'" - % (responseHref, resource)) - else: - self.assertEquals(hasSlash, False, - "failure with href '%s' while querying '%s'" - % (responseHref, resource)) - - def testPropfindURL(self): - """propfind: ensure various NSURL work-arounds""" - # a collection without / - self._testPropfindURL('/SOGo/dav/%s' % username) - # a collection with / - self._testPropfindURL('/SOGo/dav/%s/' % username) - # a non-collection - self._testPropfindURL('/SOGo/dav/%s/freebusy.ifb' % username) - - ## REPORT - def testPrincipalPropertySearch(self): - """principal-property-search""" - resource = '/SOGo/dav' - userInfo = self.dav_utility.fetchUserInfo(username) - # subscriber_userInfo = self.dav_utility.fetchUserInfo(subscriber_username) - matches = [["{urn:ietf:params:xml:ns:caldav}calendar-home-set", - "/SOGo/dav/%s/Calendar" % username]] - ## the SOGo implementation does not support more than one - ## property-search at a time: - # ["{urn:ietf:params:xml:ns:caldav}calendar-home-set", - # "/SOGo/dav/%s/Calendar" % subscriber_username]] - query = webdavlib.WebDAVPrincipalPropertySearch(resource, - ["displayname"], matches) - self.client.execute(query) - self.assertEquals(query.response["status"], 207) - response = query.response["document"].findall('{DAV:}response')[0] - href = response.find('{DAV:}href').text - self.assertEquals("/SOGo/dav/%s/" % username, href) - displayname = response.find('{DAV:}propstat/{DAV:}prop/{DAV:}displayname') - value = displayname.text - if value is None: - value = "" - self.assertEquals(userInfo[0], value) - - # http://tools.ietf.org/html/rfc3253.html#section-3.8 - def testExpandProperty(self): - """expand-property""" - resource = '/SOGo/dav/%s/' % username - userInfo = self.dav_utility.fetchUserInfo(username) - - query_props = {"{DAV:}owner": { "{DAV:}href": resource, - "{DAV:}displayname": userInfo[0]}, - "{DAV:}principal-collection-set": { "{DAV:}href": "/SOGo/dav/", - "{DAV:}displayname": "SOGo"}} - query = webdavlib.WebDAVExpandProperty(resource, query_props.keys(), - ["displayname"]) - self.client.execute(query) - self.assertEquals(query.response["status"], 207) - - topResponse = query.response["document"].find('{DAV:}response') - topHref = topResponse.find('{DAV:}href') - self.assertEquals(resource, topHref.text) - for query_prop in query_props.keys(): - propResponse = topResponse.find('{DAV:}propstat/{DAV:}prop/%s' - % query_prop) - propHref = propResponse.find('{DAV:}response/{DAV:}href') - self.assertEquals(query_props[query_prop]["{DAV:}href"], - propHref.text, - "'%s', href mismatch: exp. '%s', got '%s'" - % (query_prop, - query_props[query_prop]["{DAV:}href"], - propHref.text)) - propDisplayname = propResponse.find('{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}displayname') - displayName = propDisplayname.text - if displayName is None: - displayName = "" - self.assertEquals(query_props[query_prop]["{DAV:}displayname"], - displayName, - "'%s', displayname mismatch: exp. '%s', got '%s'" - % (query_prop, - query_props[query_prop]["{DAV:}displayname"], - propDisplayname)) - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-webdavlib.py b/Tests/Integration/test-webdavlib.py deleted file mode 100755 index 781448d14..000000000 --- a/Tests/Integration/test-webdavlib.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/python - -import sogotests -import unittest - -from webdavlib import * - -class HTTPUnparsedURLTest(unittest.TestCase): - def testURLParse(self): - fullURL = "http://username:password@hostname:123/folder/folder/object?param1=value1¶m2=value2" - testURL = HTTPUnparsedURL(fullURL) - self.assertEquals(testURL.protocol, "http") - self.assertEquals(testURL.username, "username") - self.assertEquals(testURL.password, "password") - self.assertEquals(testURL.hostname, "hostname") - self.assertEquals(testURL.port, "123") - self.assertEquals(testURL.path, "/folder/folder/object") - - exp_params = { "param1": "value1", - "param2": "value2" } - self.assertEquals(exp_params, testURL.parameters) - - pathURL = "/folder/folder/simplereference" - testURL = HTTPUnparsedURL(pathURL) - self.assertEquals(testURL.protocol, None) - self.assertEquals(testURL.username, None) - self.assertEquals(testURL.password, None) - self.assertEquals(testURL.hostname, None) - self.assertEquals(testURL.port, None) - self.assertEquals(testURL.path, "/folder/folder/simplereference") - - pathURL = "http://user:secret@bla.com/hooray" - testURL = HTTPUnparsedURL(pathURL) - self.assertEquals(testURL.protocol, "http") - self.assertEquals(testURL.username, "user") - self.assertEquals(testURL.password, "secret") - self.assertEquals(testURL.hostname, "bla.com") - self.assertEquals(testURL.port, None) - self.assertEquals(testURL.path, "/hooray") - - pathURL = "http://user@bla.com:80/hooray" - testURL = HTTPUnparsedURL(pathURL) - self.assertEquals(testURL.protocol, "http") - self.assertEquals(testURL.username, "user") - self.assertEquals(testURL.password, None) - self.assertEquals(testURL.hostname, "bla.com") - self.assertEquals(testURL.port, "80") - self.assertEquals(testURL.path, "/hooray") - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/test-webdavsync.py b/Tests/Integration/test-webdavsync.py deleted file mode 100755 index e84288136..000000000 --- a/Tests/Integration/test-webdavsync.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/python - -from config import hostname, port, username, password - -import math -import sys -import sogotests -import time -import unittest -import webdavlib - -resource = '/SOGo/dav/%s/Calendar/test-webdavsync/' % username - -class WebdavSyncTest(unittest.TestCase): - def setUp(self): - self.client = webdavlib.WebDAVClient(hostname, port, - username, password) - - def tearDown(self): - delete = webdavlib.WebDAVDELETE(resource) - self.client.execute(delete) - - def test(self): - """webdav sync""" - # missing tests: - # invalid tokens: negative, non-numeric, > current timestamp - # non-empty collections: token validity, status codes for added, - # modified and removed elements - - # preparation - mkcol = webdavlib.WebDAVMKCOL(resource) - self.client.execute(mkcol) - self.assertEquals(mkcol.response["status"], 201, - "preparation: failure creating collection (code != 201)") - - # test queries: - # empty collection: - # without a token (query1) - # with a token (query2) - # (when done, non-empty collection: - # without a token (query3) - # with a token (query4)) - - query1 = webdavlib.WebDAVSyncQuery(resource, None, [ "getetag" ]) - self.client.execute(query1) - self.assertEquals(query1.response["status"], 207, - ("query1: invalid status code: %d (!= 207)" - % query1.response["status"])) - token_node = query1.response["document"].find("{DAV:}sync-token") - # Implicit "assertion": we expect SOGo to return a token node, with a - # non-empty numerical value. Anything else will trigger an exception - token = int(token_node.text) - - self.assertTrue(token >= 0) - query1EndTime = int(math.ceil(query1.start + query1.duration)) - self.assertTrue(token <= query1EndTime, - "token = %d > query1EndTime = %d" % (token, query1EndTime)) - - # we make sure that any token is accepted when the collection is - # empty, but that the returned token differs - query2 = webdavlib.WebDAVSyncQuery(resource, "1234", [ "getetag" ]) - self.client.execute(query2) - self.assertEquals(query2.response["status"], 207) - token_node = query2.response["document"].find("{DAV:}sync-token") - self.assertTrue(token_node is not None, - "expected 'sync-token' tag") - token = int(token_node.text) - self.assertTrue(token > 0) - -if __name__ == "__main__": - sogotests.runTests() diff --git a/Tests/Integration/utilities.py b/Tests/Integration/utilities.py deleted file mode 100644 index 850cb07a8..000000000 --- a/Tests/Integration/utilities.py +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/python - -import StringIO -import sys -import unittest -import vobject -import vobject.ics_diff -import webdavlib -import xml.sax.saxutils - -class ics_compare(): - - def __init__(self, event1, event2): - self.event1 = event1 - self.event2 = event2 - self.diffs = None - - def _vcalendarComponent(self, event): - event_component = None - for item in vobject.readComponents(event): - if item.name == "VCALENDAR": - event_component = item - return event_component - - def areEqual(self): - s_event1 = StringIO.StringIO(self.event1) - s_event2 = StringIO.StringIO(self.event2) - - event1_vcalendar = self._vcalendarComponent(s_event1) - if event1_vcalendar is None: - raise Exception("No VCALENDAR component in event1") - - event2_vcalendar = self._vcalendarComponent(s_event2) - if event2_vcalendar is None: - raise Exception("No VCALENDAR component in event2") - - self.diffs = vobject.ics_diff.diff(event1_vcalendar, event2_vcalendar) - if not self.diffs: - return True - else: - return False - - def textDiff(self): - saved_stdout = sys.stdout - out = StringIO.StringIO() - sys.stdout = out - try : - if self.diffs is not None: - for (left, right) in self.diffs: - left.prettyPrint() - right.prettyPrint() - finally: - sys.stdout = saved_stdout - - return out.getvalue().strip() - - -class TestUtility(): - def __init__(self, test, client, resource = None): - self.test = test - self.client = client - self.userInfo = {} - - def fetchUserInfo(self, login): - if not self.userInfo.has_key(login): - resource = "/SOGo/dav/%s/" % login - propfind = webdavlib.WebDAVPROPFIND(resource, - ["displayname", - "{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"], - 0) - self.client.execute(propfind) - self.test.assertEquals(propfind.response["status"], 207) - common_tree = "{DAV:}response/{DAV:}propstat/{DAV:}prop" - name_nodes = propfind.response["document"] \ - .findall('%s/{DAV:}displayname' % common_tree) - email_nodes = propfind.response["document"] \ - .findall('%s/{urn:ietf:params:xml:ns:caldav}calendar-user-address-set/{DAV:}href' - % common_tree) - - if len(name_nodes[0].text) > 0: - displayName = name_nodes[0].text - else: - displayName = "" - self.userInfo[login] = (displayName, email_nodes[0].text) - - return self.userInfo[login] - -class TestACLUtility(TestUtility): - def __init__(self, test, client, resource): - TestUtility.__init__(self, test, client, resource) - self.resource = resource - - def _subscriptionOperation(self, subscribers, operation): - subscribeQuery = ("\n" - + "<%s" % operation - + " xmlns=\"urn:inverse:params:xml:ns:inverse-dav\"") - if (subscribers is not None): - subscribeQuery = (subscribeQuery - + " users=\"%s\"" % ",".join(subscribers)) - subscribeQuery = subscribeQuery + "/>" - post = webdavlib.HTTPPOST(self.resource, subscribeQuery) - post.content_type = "application/xml; charset=\"utf-8\"" - self.client.execute(post) - self.test.assertEquals(post.response["status"], 200, - "subscribtion failure to '%s' for '%s' (status: %d)" - % (self.resource, "', '".join(subscribers), - post.response["status"])) - - def subscribe(self, subscribers=None): - self._subscriptionOperation(subscribers, "subscribe") - - def unsubscribe(self, subscribers=None): - self._subscriptionOperation(subscribers, "unsubscribe") - - def rightsToSOGoRights(self, rights): - self.fail("subclass must implement this method") - - def setupRights(self, username, rights = None): - if rights is not None: - rights_str = "".join(["<%s/>" - % x for x in self.rightsToSOGoRights(rights) ]) - aclQuery = ("\n" - + "" - + "%s" % (xml.sax.saxutils.escape(username), - rights_str) - + "") - else: - aclQuery = ("\n" - + "" - + "" % xml.sax.saxutils.escape(username) - + "") - - post = webdavlib.HTTPPOST(self.resource, aclQuery) - post.content_type = "application/xml; charset=\"utf-8\"" - self.client.execute(post) - - if rights is None: - err_msg = ("rights modification: failure to remove entry (status: %d)" - % post.response["status"]) - else: - err_msg = ("rights modification: failure to set '%s' (status: %d)" - % (rights_str, post.response["status"])) - self.test.assertEquals(post.response["status"], 204, err_msg) - -# Calendar: -# rights: -# v: view all -# d: view date and time -# m: modify -# r: respond -# short rights notation: { "c": create, -# "d": delete, -# "pu": public, -# "pr": private, -# "co": confidential } -class TestCalendarACLUtility(TestACLUtility): - def rightsToSOGoRights(self, rights): - sogoRights = [] - if rights.has_key("c") and rights["c"]: - sogoRights.append("ObjectCreator") - if rights.has_key("d") and rights["d"]: - sogoRights.append("ObjectEraser") - - classes = { "pu": "Public", - "pr": "Private", - "co": "Confidential" } - rights_table = { "v": "Viewer", - "d": "DAndTViewer", - "m": "Modifier", - "r": "Responder" } - for k in classes.keys(): - if rights.has_key(k): - right = rights[k] - sogo_right = "%s%s" % (classes[k], rights_table[right]) - sogoRights.append(sogo_right) - - return sogoRights - -# Addressbook: -# short rights notation: { "c": create, -# "d": delete, -# "e": edit, -# "v": view } -class TestAddressBookACLUtility(TestACLUtility): - def rightsToSOGoRights(self, rights): - sogoRightsTable = { "c": "ObjectCreator", - "d": "ObjectEraser", - "v": "ObjectViewer", - "e": "ObjectEditor" } - - sogoRights = [] - for k in rights.keys(): - sogoRights.append(sogoRightsTable[k]) - - return sogoRights - - diff --git a/Tests/Integration/webdavlib.py b/Tests/Integration/webdavlib.py deleted file mode 100644 index 4d3d8311d..000000000 --- a/Tests/Integration/webdavlib.py +++ /dev/null @@ -1,581 +0,0 @@ -# webdavlib.py - A versatile WebDAV Python Library -# -# Copyright (C) 2009, 2010 Inverse inc. -# -# Author: Wolfgang Sourdeau -# -# webdavlib 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, or (at your option) any later -# version. -# -# webdavlib 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 webdavlib; see the file COPYING. If not, write to the Free -# Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, -# USA. - -import httplib -import re -import time -import xml.dom.expatbuilder -import xml.etree.cElementTree -import xml.sax.saxutils -import sys - -xmlns_dav = "DAV:" -xmlns_caldav = "urn:ietf:params:xml:ns:caldav" -xmlns_carddav = "urn:ietf:params:xml:ns:carddav" -xmlns_inversedav = "urn:inverse:params:xml:ns:inverse-dav" - -url_re = None - -class HTTPUnparsedURL: - def __init__(self, url): - self._parse(url) - - def _parse(self, url): - # ((proto)://((username(:(password)?)@)?hostname(:(port))))(path)? -# if url_re is None: - url_parts = url.split("?") - alpha_match = "[a-zA-Z0-9%\._-]+" - num_match = "[0-9]+" - pattern = ("((%s)://(((%s)(:(%s))?@)?(%s)(:(%s))?))?(/.*)" - % (alpha_match, alpha_match, alpha_match, - alpha_match, num_match)) - url_re = re.compile(pattern) - re_match = url_re.match(url_parts[0]) - if re_match is None: - raise Exception, "URL expression could not be parsed: %s" % url - - (trash, self.protocol, trash, trash, self.username, trash, - self.password, self.hostname, trash, self.port, self.path) = re_match.groups() - - self.parameters = {} - if len(url_parts) > 1: - param_elms = url_parts[1].split("&") - for param_pair in param_elms: - parameter = param_pair.split("=") - self.parameters[parameter[0]] = parameter[1] - -class WebDAVClient: - user_agent = "Mozilla/5.0" - - def __init__(self, hostname, port, username = None, password = "", - forcessl = False): - if int(port) == 443 or forcessl: - import M2Crypto.httpslib - self.conn = M2Crypto.httpslib.HTTPSConnection(hostname, int(port), - True) - else: - self.conn = httplib.HTTPConnection(hostname, port, True) - - if username is None: - self.simpleauth_hash = None - else: - self.simpleauth_hash = (("%s:%s" % (username, password)) - .encode('base64')[:-1]) - - def prepare_headers(self, query, body): - headers = { "User-Agent": self.user_agent } - if self.simpleauth_hash is not None: - headers["authorization"] = "Basic %s" % self.simpleauth_hash - if body is not None: - headers["content-length"] = len(body) - if query.__dict__.has_key("depth") and query.depth is not None: - headers["depth"] = query.depth - if query.__dict__.has_key("content_type"): - headers["content-type"] = query.content_type - if not query.__dict__.has_key("accept-language"): - headers["accept-language"] = 'en-us,en;q=0.5' - - query_headers = query.prepare_headers() - if query_headers is not None: - for key in query_headers.keys(): - headers[key] = query_headers[key] - - return headers - - def execute(self, query): - body = query.render() - - query.start = time.time() - self.conn.request(query.method, query.url, - body, self.prepare_headers(query, body)) - try: - query.set_response(self.conn.getresponse()); - except httplib.BadStatusLine, e: - print e - time.sleep(3) - query.set_response(self.conn.getresponse()); - query.duration = time.time() - query.start - -class HTTPSimpleQuery: - method = None - - def __init__(self, url): - self.url = url - self.response = None - self.start = -1 - self.duration = -1 - - def prepare_headers(self): - return {} - - def render(self): - return None - - def set_response(self, http_response): - headers = {} - for rk, rv in http_response.getheaders(): - k = rk.lower() - headers[k] = rv - self.response = { "headers": headers, - "status": http_response.status, - "version": http_response.version, - "body": http_response.read() } - -class HTTPGET(HTTPSimpleQuery): - method = "GET" - cookie = None - - def prepare_headers (self): - headers = HTTPSimpleQuery.prepare_headers(self) - if self.cookie: - headers["Cookie"] = self.cookie - return headers - - -class HTTPOPTIONS(HTTPSimpleQuery): - method = "OPTIONS" - -class HTTPQuery(HTTPSimpleQuery): - def __init__(self, url): - HTTPSimpleQuery.__init__(self, url) - self.content_type = "application/octet-stream" - -class HTTPPUT(HTTPQuery): - method = "PUT" - - def __init__(self, url, content, - content_type="application/octet-stream", - exclusive=False): - HTTPQuery.__init__(self, url) - self.content = content - self.content_type = content_type - self.exclusive = exclusive - - def render(self): - return self.content - - def prepare_headers(self): - headers = HTTPQuery.prepare_headers(self) - if self.exclusive: - headers["if-none-match"] = "*" - - return headers - -class HTTPPOST(HTTPPUT): - method = "POST" - cookie = None - - def prepare_headers (self): - headers = HTTPPUT.prepare_headers(self) - if self.cookie: - headers["Cookie"] = self.cookie - return headers - - - -class WebDAVQuery(HTTPQuery): - method = None - - def __init__(self, url, depth = None): - HTTPQuery.__init__(self, url) - self.content_type = "application/xml; charset=\"utf-8\"" - self.depth = depth - self.ns_mgr = _WD_XMLNS_MGR() - self.top_node = None - - # helper for PROPFIND and REPORT (only) - def _initProperties(self, properties): - props = _WD_XMLTreeElement("prop") - self.top_node.append(props) - for prop in properties: - prop_tag = self.render_tag(prop) - props.append(_WD_XMLTreeElement(prop_tag)) - - def render(self): - if self.top_node is not None: - text = ("\n%s" - % self.top_node.render(self.ns_mgr.render())) - else: - text = "" - - return text - - def render_tag(self, tag): - cb = tag.find("}") - if cb > -1: - ns = tag[1:cb] - real_tag = tag[cb+1:] - new_tag = self.ns_mgr.register(real_tag, ns) - else: - new_tag = tag - - return new_tag - - def set_response(self, http_response): - HTTPQuery.set_response(self, http_response) - headers = self.response["headers"] - if (headers.has_key("content-type") - and headers.has_key("content-length") - and (headers["content-type"].startswith("application/xml") - or headers["content-type"].startswith("text/xml")) - and int(headers["content-length"]) > 0): - document = xml.etree.cElementTree.fromstring(self.response["body"]) - self.response["document"] = document - -class WebDAVMKCOL(WebDAVQuery): - method = "MKCOL" - -class WebDAVDELETE(WebDAVQuery): - method = "DELETE" - -class WebDAVREPORT(WebDAVQuery): - method = "REPORT" - -class WebDAVGET(WebDAVQuery): - method = "GET" - -class WebDAVPROPFIND(WebDAVQuery): - method = "PROPFIND" - - def __init__(self, url, properties, depth = None): - WebDAVQuery.__init__(self, url, depth) - self.top_node = _WD_XMLTreeElement("propfind") - if properties is not None and len(properties) > 0: - self._initProperties(properties) - -class WebDAVPROPPATCH(WebDAVQuery): - method = "PROPPATCH" - -# - - def __init__(self, url, properties): - WebDAVQuery.__init__(self, url, None) - self.top_node = _WD_XMLTreeElement("propertyupdate") - set_node = _WD_XMLTreeElement("set") - self.top_node.append(set_node) - prop_node = _WD_XMLTreeElement("prop") - set_node.append(prop_node) - - prop_node.appendSubtree(self, properties) - -class WebDAVMOVE(WebDAVQuery): - method = "MOVE" - destination = None - host = None - - def prepare_headers(self): - headers = WebDAVQuery.prepare_headers(self) - print "DESTINATION", self.destination - if self.destination is not None: - headers["Destination"] = self.destination - if self.host is not None: - headers["Host"] = self.host - return headers - -class WebDAVPrincipalPropertySearch(WebDAVREPORT): - def __init__(self, url, properties, matches): - WebDAVQuery.__init__(self, url) - ppsearch_tag = self.ns_mgr.register("principal-property-search", - xmlns_dav) - self.top_node = _WD_XMLTreeElement(ppsearch_tag) - self._initMatches(matches) - if properties is not None and len(properties) > 0: - self._initProperties(properties) - - def _initMatches(self, matches): - for match in matches: - psearch = _WD_XMLTreeElement("property-search") - self.top_node.append(psearch) - prop = _WD_XMLTreeElement("prop") - psearch.append(prop) - match_tag = self.render_tag(match[0]) - prop.append(_WD_XMLTreeElement(match_tag)) - match_tag = _WD_XMLTreeElement("match") - psearch.append(match_tag) - match_tag.appendSubtree(self, match[1]) - -class WebDAVSyncQuery(WebDAVREPORT): - def __init__(self, url, token, properties): - WebDAVQuery.__init__(self, url) - self.top_node = _WD_XMLTreeElement("sync-collection") - - sync_token = _WD_XMLTreeElement("sync-token") - self.top_node.append(sync_token) - if token is not None: - sync_token.append(_WD_XMLTreeTextNode(token)) - - if properties is not None and len(properties) > 0: - self._initProperties(properties) - -class WebDAVExpandProperty(WebDAVREPORT): - def _parseTag(self, tag): - result = [] - - cb = tag.find("}") - if cb > -1: - result.append(tag[cb+1:]) - result.append(tag[1:cb]) - else: - result.append(tag) - result.append("DAV:") - - return result; - - def _propElement(self, tag): - parsedTag = self._parseTag(tag) - parameters = { "name": parsedTag[0] } - if len(parsedTag) > 1: - parameters["namespace"] = parsedTag[1] - - return _WD_XMLTreeElement("property", parameters) - - def __init__(self, url, query_properties, properties): - WebDAVQuery.__init__(self, url) - self.top_node = _WD_XMLTreeElement("expand-property") - - for query_tag in query_properties: - property_query = self._propElement(query_tag) - self.top_node.append(property_query) - for tag in properties: - property = self._propElement(tag) - property_query.append(property) - -class CalDAVPOST(WebDAVQuery): - method = "POST" - - def __init__(self, url, content, - originator = None, recipients = None): - WebDAVQuery.__init__(self, url) - self.content_type = "text/calendar; charset=utf-8" - self.originator = originator - self.recipients = recipients - self.content = content - - def prepare_headers(self): - headers = WebDAVQuery.prepare_headers(self) - - if self.originator is not None: - headers["originator"] = self.originator - - if self.recipients is not None: - headers["recipient"] = ",".join(self.recipients) - - return headers - - def render(self): - return self.content - -class CalDAVCalendarMultiget(WebDAVREPORT): - def __init__(self, url, properties, hrefs, depth = None): - WebDAVQuery.__init__(self, url, depth) - multiget_tag = self.ns_mgr.register("calendar-multiget", xmlns_caldav) - self.top_node = _WD_XMLTreeElement(multiget_tag) - if properties is not None and len(properties) > 0: - self._initProperties(properties) - - for href in hrefs: - href_node = _WD_XMLTreeElement("href") - self.top_node.append(href_node) - href_node.append(_WD_XMLTreeTextNode(href)) - -class CalDAVCalendarQuery(WebDAVREPORT): - def __init__(self, url, properties, component = None, timerange = None): - WebDAVQuery.__init__(self, url) - multiget_tag = self.ns_mgr.register("calendar-query", xmlns_caldav) - self.top_node = _WD_XMLTreeElement(multiget_tag) - if properties is not None and len(properties) > 0: - self._initProperties(properties) - - if component is not None: - filter_tag = self.ns_mgr.register("filter", - xmlns_caldav) - compfilter_tag = self.ns_mgr.register("comp-filter", - xmlns_caldav) - filter_node = _WD_XMLTreeElement(filter_tag) - cal_filter_node = _WD_XMLTreeElement(compfilter_tag, - { "name": "VCALENDAR" }) - comp_node = _WD_XMLTreeElement(compfilter_tag, - { "name": component }) - ## TODO - # if timerange is not None: - cal_filter_node.append(comp_node) - filter_node.append(cal_filter_node) - self.top_node.append(filter_node) - -class CardDAVAddressBookQuery(WebDAVREPORT): - def __init__(self, url, properties, searchProperty = None, searchValue = None): - WebDAVQuery.__init__(self, url) - query_tag = self.ns_mgr.register("addressbook-query", xmlns_carddav) - ns_key = self.ns_mgr.xmlns[xmlns_carddav] - self.top_node = _WD_XMLTreeElement(query_tag) - if properties is not None and len(properties) > 0: - self._initProperties(properties) - - if searchProperty is not None: - filter_node = _WD_XMLTreeElement("%s:filter" % ns_key) - self.top_node.append(filter_node) - propfilter_node = _WD_XMLTreeElement("%s:prop-filter" % ns_key, { "name": searchProperty }) - filter_node.append(propfilter_node) - match_node = _WD_XMLTreeElement("%s:text-match" % ns_key, - { "collation": "i;unicasemap", "match-type": "starts-with" }) - propfilter_node.append(match_node) - match_node.appendSubtree(None, searchValue) - -class MailDAVMailQuery(WebDAVREPORT): - def __init__(self, url, properties, filters = None, - sort = None, ascending = True): - WebDAVQuery.__init__(self, url) - mailquery_tag = self.ns_mgr.register("mail-query", - xmlns_inversedav) - self.top_node = _WD_XMLTreeElement(mailquery_tag) - if properties is not None and len(properties) > 0: - self._initProperties(properties) - - if filters is not None and len(filters) > 0: - self._initFilters(filters) - - if sort is not None and len(sort) > 0: - self._initSort(sort, ascending) - - def _initFilters(self, filters): - mailfilter_tag = self.ns_mgr.register("mail-filters", - xmlns_inversedav) - mailfilter_node = _WD_XMLTreeElement(mailfilter_tag) - self.top_node.append(mailfilter_node) - for filterk in filters.keys(): - filter_tag = self.ns_mgr.register(filterk, - xmlns_inversedav) - filter_node = _WD_XMLTreeElement(filter_tag, - filters[filterk]) - mailfilter_node.append(filter_node) - - def _initSort(self, sort, ascending): - sort_tag = self.ns_mgr.register("sort", xmlns_inversedav) - if ascending: - sort_attrs = { "order": "ascending" } - else: - sort_attrs = { "order": "descending" } - sort_node = _WD_XMLTreeElement(sort_tag, sort_attrs) - self.top_node.append(sort_node) - - for item in sort: - sort_subnode = _WD_XMLTreeElement(self.render_tag(item)) - sort_node.append(sort_subnode) - -# private classes to handle XML stuff -class _WD_XMLNS_MGR: - def __init__(self): - self.xmlns = {} - self.counter = 0 - - def render(self): - text = " xmlns=\"DAV:\"" - for k in self.xmlns: - text = text + " xmlns:%s=\"%s\"" % (self.xmlns[k], k) - - return text - - def create_key(self, namespace): - new_nssym = "n%d" % self.counter - self.counter = self.counter + 1 - self.xmlns[namespace] = new_nssym - - return new_nssym - - def register(self, tag, namespace): - if namespace != xmlns_dav: - if self.xmlns.has_key(namespace): - key = self.xmlns[namespace] - else: - key = self.create_key(namespace) - else: - key = None - - if key is not None: - newTag = "%s:%s" % (key, tag) - else: - newTag = tag - - return newTag - -class _WD_XMLTreeElement: - typeNum = type(0) - typeStr = type("") - typeUnicode = type(u"") - typeList = type([]) - typeDict = type({}) - - def __init__(self, tag, attributes = {}): - self.tag = tag - self.children = [] - self.attributes = attributes - - def append(self, child): - self.children.append(child) - - def appendSubtree(self, query, subtree): - if type(subtree) == self.typeNum: - strValue = "%d" % subtree - textNode = _WD_XMLTreeTextNode(strValue) - self.append(textNode) - elif type(subtree) == self.typeUnicode: - textNode = _WD_XMLTreeTextNode(subtree.encode("utf-8")) - self.append(textNode) - elif type(subtree) == self.typeStr: - textNode = _WD_XMLTreeTextNode(subtree) - self.append(textNode) - elif type(subtree) == self.typeList: - for x in subtree: - self.appendSubtree(query, x) - elif type(subtree) == self.typeDict: - for x in subtree.keys(): - tag = query.render_tag(x) - node = _WD_XMLTreeElement(tag) - node.appendSubtree(query, subtree[x]) - self.append(node) - else: - None - - def render(self, ns_text = None): - text = "<" + self.tag - - if ns_text is not None: - text = text + ns_text - - for k in self.attributes: - text = text + " %s=\"%s\"" % (k, self.attributes[k]) - - if len(self.children) > 0: - text = text + ">" - for child in self.children: - text = text + child.render() - text = text + "" - else: - text = text + "/>" - - return text - -class _WD_XMLTreeTextNode: - def __init__(self, text): - self.text = xml.sax.saxutils.escape(text) - - def render(self): - return self.text diff --git a/Tests/Integration/webdavsync-tool.py b/Tests/Integration/webdavsync-tool.py deleted file mode 100644 index 8aa2a2729..000000000 --- a/Tests/Integration/webdavsync-tool.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/python - -import getopt -import sys -import urlparse -import webdavlib -import xml.dom.minidom - -def usage() : - msg ="""Usage: - %s [-h] [-s sync-token] -u uri\n""" % sys.argv[0] - - sys.stderr.write(msg); - -def getAllCollections(client, uri): - collections = [] - depth = 1 - - propfind = webdavlib.WebDAVPROPFIND(uri, ["allprop"], depth) - client.execute(propfind) - client.conn.close() - doc = propfind.response["document"] - for response in doc.iter("{DAV:}response"): - propstat = response.find("{DAV:}propstat") - if propstat is not None: - prop = propstat.find("{DAV:}prop") - if prop is not None: - resourcetype = prop.find("{DAV:}resourcetype") - if resourcetype.find("{DAV:}collection") is not None: - href = prop.find("{DAV:}href") - if href is not None and href.text != uri: - collections.append(href.text) - return collections - -def changedItemsFromCollection(client, collection, synctoken=None): - # get all changed hrefs since synctoken - hrefs = [] - syncquery = webdavlib.WebDAVSyncQuery(collection, synctoken, [ "getcontenttype", "getetag" ]) - client.execute(syncquery) - client.conn.close() - if (syncquery.response["status"] != 207): - raise Exception("Bad http response code: %d" % syncquery.response["status"]) - doc = syncquery.response["document"] - - # extract all hrefs - for syncResponse in doc.iter("{DAV:}response"): - href = syncResponse.find("{DAV:}href") - if href is not None: - hrefs.append(href.text) - - return hrefs - - -def main(): - depth = 1 - synctoken = "1" - url = None - - try: - opts, args = getopt.getopt (sys.argv[1:], "hs:u:", \ - ("sync-token=", "url=")); - except getopt.GetoptError: - usage() - exit(1) - - for o, v in opts : - if o == "-h" : - usage() - exit(1) - elif o == "-s" or o == "--sync-token" : - synctoken = v - elif o == "-u" or o == "--url" : - url = v - - if url is None: - usage() - exit(1) - - o = urlparse.urlparse(url) - hostname = o.hostname - port = o.port - username = o.username - password = o.password - uri = o.path - - client = webdavlib.WebDAVClient(hostname, port, username, password) - - collections = getAllCollections(client, uri) - if len(collections) == 0: - print "No collections found!" - sys.exit(1) - - for collection in collections: - changedItems = changedItemsFromCollection(client, collection) - # fetch the href data - if len(changedItems) > 0: - multiget = webdavlib.CalDAVCalendarMultiget(collection, - ["getetag", "{%s}calendar-data" % webdavlib.xmlns_caldav], - changedItems, depth) - client.execute(multiget) - client.conn.close() - if (multiget.response["status"] != 207): - raise Exception("Bad http response code: %d" % multiget.response["status"]) - -if __name__ == "__main__": - main() diff --git a/Tests/README.md b/Tests/README.md new file mode 100644 index 000000000..b572fdcea --- /dev/null +++ b/Tests/README.md @@ -0,0 +1,16 @@ +# Tests + +This directory holds automated tests for SOGo. + + - `spec` and `lib`: hold JavaScript driven interated tests that are used to validate overall DAV functionality + - `Unit`: holds all unit tests + +The configuration is found in `lib/config.js`. + +## Tools + +* [Jasmin](https://jasmine.github.io/) - testing framework +* [tsdav](https://tsdav.vercel.app/) - webdav request helper +* [ical.js](https://github.com/mozilla-comm/ical.js) - ics and vcard parser +* [cross-fetch](https://github.com/lquixada/cross-fetch) - fetch API +* [xml-js](https://github.com/nashwaan/xml-js) - convert JS object to XML diff --git a/Tests/lib/WebDAV.js b/Tests/lib/WebDAV.js new file mode 100644 index 000000000..36c2fb09f --- /dev/null +++ b/Tests/lib/WebDAV.js @@ -0,0 +1,557 @@ +import cookie from 'cookie' +import { + DAVNamespace, + DAVNamespaceShorthandMap, + + davRequest, + deleteObject, + formatProps, + getBasicAuthHeaders, + getDAVAttribute, + propfind, + + calendarMultiGet, + createCalendarObject, + makeCalendar, + + createVCard +} from 'tsdav' +import convert from 'xml-js' +import { fetch } from 'cross-fetch' +import config from './config' + +const DAVInverse = 'urn:inverse:params:xml:ns:inverse-dav' +const DAVInverseShort = 'i' +const DAVMailHeader = 'urn:schemas:mailheader:' +const DAVMailHeaderShort = 'mh' +const DAVHttpMail = 'urn:schemas:httpmail:' +const DAVHttpMailShort = 'hm' +const DAVnsShortMap = { + [DAVInverse]: DAVInverseShort, + [DAVMailHeader]: DAVMailHeaderShort, + [DAVHttpMail]: DAVHttpMailShort, + ...DAVNamespaceShorthandMap +} + +export { + DAVInverse, DAVInverseShort, + DAVMailHeader, DAVMailHeaderShort, + DAVHttpMail, DAVHttpMailShort +} + +class WebDAV { + constructor(un, pw) { + this.serverUrl = `http://${config.hostname}:${config.port}` + this.cookie = null + if (un && pw) { + this.username = un + this.password = pw + this.headers = getBasicAuthHeaders({ + username: un, + password: pw + }) + } + else { + this.headers = {} + } + } + + // Generic operations + + async getAuthCookie() { + if (!this.cookie) { + const resource = `/SOGo/connect` + const response = await fetch(this.serverUrl + resource, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({userName: this.username, password: this.password}) + }) + const values = response.headers.get('set-cookie').split(/, /) + let authCookies = [] + for (let v of values) { + let c = cookie.parse(v) + for (let authCookie of ['0xHIGHFLYxSOGo', 'XSRF-TOKEN']) { + if (Object.keys(c).includes('0xHIGHFLYxSOGo')) { + authCookies.push(cookie.serialize(authCookie, c[authCookie])) + } + } + } + this.cookie = authCookies.join('; ') + } + return this.cookie + } + + async getHttp(resource) { + const authCookie = await this.getAuthCookie() + const localHeaders = { Cookie: authCookie } + + return await fetch(this.serverUrl + resource, { + method: 'GET', + headers: localHeaders + }) + } + + async postHttp(resource, contentType = 'application/json', data = '') { + const authCookie = await this.getAuthCookie() + const localHeaders = { 'Content-Type': contentType, Cookie: authCookie } + + return await fetch(this.serverUrl + resource, { + method: 'POST', + body: data, + headers: localHeaders + }) + } + + // WebDAV operations + + deleteObject(resource) { + return deleteObject({ + url: this.serverUrl + resource, + headers: this.headers + }) + } + + getObject(resource, filename) { + let url + if (resource.match(/^http/)) + url = resource + else + url = this.serverUrl + resource + if (filename) + url += filename + return davRequest({ + url, + init: { + method: 'GET', + headers: this.headers, + body: null + }, + convertIncoming: false + }) + } + + makeCollection(resource) { + return davRequest({ + url: this.serverUrl + resource, + init: { + method: 'MKCOL', + headers: this.headers, + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV] + } + }) + } + + propfindWebdav(resource, properties, namespace = DAVNamespace.DAV, headers = {}) { + const nsShort = DAVnsShortMap[namespace] || DAVInverseShort + const formattedProperties = properties.map(p => { + return { [`${nsShort}:${p}`]: '' } + }) + let url + if (resource.match(/^http/)) + url = resource + else + url = this.serverUrl + resource + if (typeof headers.depth == 'undefined') { + headers.depth = new String(0) + } + return davRequest({ + url, + init: { + method: 'PROPFIND', + headers: { ...this.headers, ...headers }, + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + body: { + propfind: { + _attributes: { + ...getDAVAttribute([ + DAVNamespace.CALDAV, + DAVNamespace.CALDAV_APPLE, + DAVNamespace.CALENDAR_SERVER, + DAVNamespace.CARDDAV, + DAVNamespace.DAV + ]), + [`xmlns:${nsShort}`]: namespace + }, + prop: formattedProperties + } + } + } + }) + } + + propfindWebdavRaw(resource, properties, headers = {}) { + const namespace = DAVNamespaceShorthandMap[DAVNamespace.DAV] + const formattedProperties = properties.map(prop => { + return { [`${namespace}:${prop}`]: '' } + }) + + let xmlBody = convert.js2xml( + { + propfind: { + _attributes: getDAVAttribute([DAVNamespace.DAV]), + prop: formattedProperties + } + }, + { + compact: true, + spaces: 2, + elementNameFn: (name) => { + // add namespace to all keys without namespace + if (!/^.+:.+/.test(name)) { + return `${namespace}:${name}`; + } + return name; + }, + } + ) + + return fetch(this.serverUrl + resource, { + headers: { + 'Content-Type': 'application/xml; charset="utf-8"', + ...headers, + ...this.headers + }, + method: 'PROPFIND', + body: xmlBody + }) + } + + propfindURL(resource = '/SOGo/dav') { + return propfind({ + url: this.serverUrl + resource, + depth: '1', + props: [ + { name: 'displayname', namespace: DAVNamespace.DAV }, + { name: 'resourcetype', namespace: DAVNamespace.DAV } + ], + headers: this.headers + }) + } + + propfindCollection(resource) { + return propfind({ + url: this.serverUrl + resource, + headers: this.headers + }) + } + + // http://tools.ietf.org/html/rfc3253.html#section-3.8 + expendProperty(resource, properties) { + return davRequest({ + url: this.serverUrl + resource, + init: { + method: 'REPORT', + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + headers: this.headers, + body: { + 'expand-property': { + _attributes: getDAVAttribute([ + DAVNamespace.DAV, + ]), + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:property`]: properties + } + } + }, + }) + } + + proppatchWebdav(resource, properties, namespace = DAVNamespace.DAV, headers = {}) { + const nsShort = DAVNamespaceShorthandMap[namespace] || DAVInverseShort + const formattedProperties = Object.keys(properties).map(p => { + if (Array.isArray(properties[p])) { + return { [`${nsShort}:${p}`]: properties[p].map(pp => { + const [ key ] = Object.keys(pp) + return { [`${nsShort}:${key}`]: pp[key] || '' } + })} + } + return { [`${nsShort}:${p}`]: properties[p] || '' } + }) + if (typeof headers.depth == 'undefined') { + headers.depth = new String(0) + } + return davRequest({ + url: this.serverUrl + resource, + init: { + method: 'PROPPATCH', + headers: { ...this.headers, ...headers }, + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + body: { + propertyupdate: { + _attributes: { + ...getDAVAttribute([ + DAVNamespace.CALDAV, + DAVNamespace.CALDAV_APPLE, + DAVNamespace.CALENDAR_SERVER, + DAVNamespace.CARDDAV, + DAVNamespace.DAV + ]), + [`xmlns:${nsShort}`]: namespace + }, + set: { + prop: formattedProperties + } + } + } + } + }) + } + + currentUserPrivilegeSet(resource) { + return propfind({ + url: this.serverUrl + resource, + depth: '0', + props: [ + { name: 'current-user-privilege-set', namespace: DAVNamespace.DAV } + ], + headers: this.headers + }) + } + + options(resource) { + return davRequest({ + url: this.serverUrl + resource, + init: { + method: 'OPTIONS', + headers: this.headers, + body: null + }, + convertIncoming: false + }) + } + + principalCollectionSet(resource = '/SOGo/dav') { + return propfind({ + url: this.serverUrl + resource, + depth: '0', + props: [{ name: 'principal-collection-set', namespace: DAVNamespace.DAV }], + headers: this.headers + }) + } + + // https://datatracker.ietf.org/doc/html/rfc6578#section-3.2 + syncCollectionRaw(resource, token = '', properties) { + const formattedProperties = properties.map((p) => { + return { [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${p}`]: '' } + }); + let xmlBody = convert.js2xml( + { + 'sync-collection': { + _attributes: getDAVAttribute([DAVNamespace.DAV]), + 'sync-level': 1, + 'sync-token': token, + prop: formattedProperties + } + }, + { + compact: true, + spaces: 2, + elementNameFn: (name) => { + // add namespace to all keys without namespace + if (!/^.+:.+/.test(name)) { + return `${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${name}` + } + return name + } + } + ) + return fetch(this.serverUrl + resource, { + headers: { + 'Content-Type': 'application/xml; charset="utf-8"', + ...this.headers + }, + method: 'REPORT', + body: xmlBody + }) + } + + // CalDAV operations + + makeCalendar(resource) { + return makeCalendar({ + url: this.serverUrl + resource, + headers: this.headers + }) + } + + createCalendarObject(resource, filename, calendar) { + return createCalendarObject({ + headers: this.headers, + calendar: { url: this.serverUrl + resource }, // DAVCalendar + filename: filename, + iCalString: calendar + }) + } + + postCaldav(resource, vcalendar, originator, recipients) { + let localHeaders = { 'content-type': 'text/calendar; charset=utf-8'} + + if (originator) + localHeaders.originator = originator + if (recipients && recipients.length > 0) + localHeaders.recipients = recipients.join(',') + + return fetch(this.serverUrl + resource, { + method: 'POST', + body: vcalendar, + headers: { ...this.headers, ...localHeaders } + }) + } + + propfindEvent(resource) { + return propfind({ + url: this.serverUrl + resource, + headers: this.headers, + depth: '1', + props: [ + { name: 'calendar-data', namespace: DAVNamespace.CALDAV } + ] + }) + } + + calendarMultiGet(resource, filename) { + return calendarMultiGet({ + url: this.serverUrl + resource, + headers: this.headers, + props: [ + { name: 'calendar-data', namespace: DAVNamespace.CALDAV }, + ], + objectUrls: [ this.serverUrl + resource + filename ] + }) + } + + principalPropertySearch(resource) { + return davRequest({ + url: `${this.serverUrl}/SOGo/dav`, + init: { + method: 'REPORT', + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + headers: this.headers, + body: { + 'principal-property-search': { + _attributes: getDAVAttribute([ + DAVNamespace.CALDAV, + DAVNamespace.DAV, + ]), + 'property-search': [ + { + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'calendar-home-set', namespace: DAVNamespace.CALDAV }]), + 'match': resource + } + ], + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'displayname', namespace: DAVNamespace.DAV }]) + } + } + }, + }) + } + + syncCollection(resource) { + return davRequest({ + url: this.serverUrl + resource, + init: { + method: 'REPORT', + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + headers: this.headers, + body: { + 'sync-collection': { + _attributes: getDAVAttribute([ + DAVNamespace.CALDAV, + DAVNamespace.DAV + ]), + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'calendar-data', namespace: DAVNamespace.CALDAV }]), + } + } + }, + }) + } + + propfindCaldav(resource, properties, depth = 0) { + return this.propfindWebdav(resource, properties, DAVNamespace.CALDAV, { depth: new String(depth) }) + } + + proppatchCaldav(resource, properties, headers = {}) { + return this.proppatchWebdav(resource, properties, DAVNamespace.CALDAV, headers) + } + + // CardDAV operations + + getCard(resource, filename) { + return davRequest({ + url: this.serverUrl + resource + filename, + init: { + method: 'GET', + headers: this.headers, + body: null + }, + convertIncoming: false + }) + } + + createVCard(resource, filename, card) { + return createVCard({ + headers: this.headers, + addressBook: { url: this.serverUrl + resource }, // DAVAddressBook + filename, + vCardString: card + }) + } + + // MailDAV operations + + mailQueryMaildav(resource, properties, filters = {}, sort, ascending = true) { + let formattedFilters = {} + if (filters) { + if (filters.constructor.toString().includes('Array')) { + filters.map(f => { + Object.keys(f).map(p => { + const pName = `${DAVInverseShort}:${p}` + if (!formattedFilters[pName]) + formattedFilters[pName] = [] + formattedFilters[pName].push({ _attributes: f[p] }) + }) + }) + } + else { + Object.keys(filters).map(p => { + const pName = `${DAVInverseShort}:${p}` + if (!formattedFilters[pName]) + formattedFilters[pName] = [] + formattedFilters[pName].push({ _attributes: filters[p] }) + }) + } + if (Object.keys(formattedFilters).length) { + formattedFilters = {[`${DAVInverseShort}:mail-filters`]: formattedFilters} + } + } + let formattedSort = {} + if (sort) { + formattedSort = {[`${DAVInverseShort}:sort`]: { + _attributes: { order: ascending ? 'ascending' : 'descending' }, + [sort]: {} + }} + } + return davRequest({ + url: this.serverUrl + resource, + init: { + method: 'REPORT', + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + headers: this.headers, + body: { + [`${DAVInverseShort}:mail-query`]: { + _attributes: { + ...getDAVAttribute([DAVNamespace.DAV]), + [`xmlns:${DAVInverseShort}`]: DAVInverse, + [`xmlns:${DAVMailHeaderShort}`]: DAVMailHeader + }, + prop: formatProps(properties.map(p => { return { name: p } })), + ...formattedFilters, + ...formattedSort + } + } + } + }) + } + +} + +export default WebDAV \ No newline at end of file diff --git a/Tests/lib/config.js b/Tests/lib/config.js new file mode 100644 index 000000000..a630cfadc --- /dev/null +++ b/Tests/lib/config.js @@ -0,0 +1,46 @@ +export default { + // setup: 4 user are needed: username, superuser, attendee1, attendee1_delegate + // superuser must be a sogo superuser... + + hostname: "localhost", + port: "20000", + username: "mysql1", + password: "qwerty", + + superuser: "francis", + superuser_password: "qwerty", + + // 'subscriber_username' and 'attendee1' must be the same user + subscriber_username: "sogo2", + subscriber_password: "sogo2", + + attendee1: "sogo2@inverse.ca", + attendee1_username: "sogo2", + attendee1_password: "sogo2", + + attendee1_delegate: "sogo3@inverse.ca", + attendee1_delegate_username: "sogo3", + attendee1_delegate_password: "qwerty", + + resource_no_overbook: "resource1", + resource_can_overbook: "resource2", + + // must match username + white_listed_attendee: { + // "sogo3": "Bob " + //"sogo1": "Bob " + "mysql1": "Bob " + }, + + mailserver: "localhost", + + testput_nbrdays: 30, + + sieve_server: "localhost", + sieve_port: 4190, + + sogo_user: "francis", + sogo_tool_path: "/home/francis/GNUstep/Tools/Admin/sogo-tool", + + webCalendarURL: "http://inverse.ca/sogo-integration-tests/CanadaHolidays.ics" +} diff --git a/Tests/lib/utilities.js b/Tests/lib/utilities.js new file mode 100644 index 000000000..d5904fd98 --- /dev/null +++ b/Tests/lib/utilities.js @@ -0,0 +1,213 @@ +import { + DAVNamespace, + davRequest, + propfind +} from 'tsdav' +import ICAL from 'ical.js' + +class TestUtility { + constructor(webdav) { + this.webdav = webdav + this.userInfo = {} + } + + async fetchUserInfo(login) { + if (!this.userInfo[login]) { + const results = await propfind({ + url: `${this.webdav.serverUrl}/SOGo/dav/${login}/`, + props: [ + { name: 'displayname', namespace: DAVNamespace.DAV }, + { name: 'calendar-timezone', namespace: DAVNamespace.CALDAV }, + { name: 'calendar-user-address-set', namespace: DAVNamespace.CALDAV } + ], + depth: '0', + headers: this.webdav.headers, + }) + if (results.length != 1) { + throw new Error(`Unexpected number of status in profind for user ${login}`) + } + + const response = results[0] + if (!response.props.calendarUserAddressSet.href.length) { + throw new Error(`No address found in calendar-user-address-set for user ${login}`) + } + + let displayname = response.props.displayname || '' + let email = response.props.calendarUserAddressSet.href[0] + let timezone = response.props.calendarTimezone || '' + this.userInfo[login] = { displayname, email, timezone } + } + return this.userInfo[login] + } + + formatTemplate(template, vars) { + var s = template + Object.keys(vars).forEach(k => { + s = s.replace(new RegExp(`%\\(${k}\\)`, 'g'), vars[k]) + }) + s = s.replace(/%\([^\)]+\)/g, '') // clear all reminding placeholders + return s; + } + + camelCase(snakeCase) { + return snakeCase.replace(/(?:^\w|[A-Z]|\b\w)/g, (char, i) => + i === 0 ? char.toLowerCase() : char.toUpperCase(), + ) + .replace(/[\s\-_]+/g, '') + } + + setupRights(resource, username, rights) { + const action = (typeof rights == 'undefined') ? 'remove-user' : 'set-roles' + return davRequest({ + url: `${this.webdav.serverUrl}${resource}`, + init: { + method: 'POST', + headers: this.webdav.headers, + body: { + 'acl-query': { + _attributes: { xmlns: 'urn:inverse:params:xml:ns:inverse-dav' }, + [action]: { + _attributes: { user: username.replace('<', '<').replace('>', '>') }, + ...rights + } + } + } + } + }) + } + + setupCalendarRights(resource, username, rights) { + let sogoRights = {} + if (rights.c) + sogoRights.ObjectCreator = {} + if (rights.d) + sogoRights.ObjectEraser = {} + + const classes = { + pu: 'Public', + pr: 'Private', + co: 'Confidential' + } + const rightsTable = { + v: 'Viewer', + d: 'DAndTViewer', + m: 'Modifier', + r: 'Responder' + } + Object.keys(classes).forEach(c => { + if (rights[c]) { + const right = rights[c] + const sogoRight = `${classes[c]}${rightsTable[right]}` + sogoRights[sogoRight] = {} + } + }) + + return this.setupRights(resource, username, sogoRights) + } + + setupAddressBookRights(resource, username, rights) { + let sogoRights = {} + + const rightsTable = { + c: 'ObjectCreator', + d: 'ObjectEraser', + v: 'ObjectViewer', + e: 'ObjectEditor' + } + Object.keys(rights).forEach(r => { + if (rightsTable[r]) { + sogoRights[rightsTable[r]] = {} + } + }) + + return this.setupRights(resource, username, sogoRights) + } + + _subscriptionOperation(resource, subscribers, operation) { + return davRequest({ + url: `${this.webdav.serverUrl}${resource}`, + init: { + method: 'POST', + headers: { + 'Content-Type': 'application/xml; charset="utf-8"', + ...this.webdav.headers + }, + body: { + [operation]: { + _attributes: { + xmlns: 'urn:inverse:params:xml:ns:inverse-dav', + users: subscribers.join(',') + } + } + } + } + }) + } + + subscribe(resource, subscribers) { + return this._subscriptionOperation(resource, subscribers, 'subscribe') + } + + unsubscribe(resource, subscribers) { + return this._subscriptionOperation(resource, subscribers, 'unsubscribe') + } + + versitDict(component) { + const comp = ICAL.Component.fromString(component) + let props = {} + + for (const prop of comp.getAllProperties()) { + props[prop.name] = prop.toICALString() + } + for (const subcomp of comp.getAllSubcomponents()) { + for (const prop of subcomp.getAllProperties()) { + props[prop.name] = prop.toICALString() + } + } + return props + } + + componentsAreEqual(comp1, comp2) { + const props1 = this.versitDict(comp1) + const props2 = this.versitDict(comp2) + for (const prop of Object.keys(props1)) { + if (props1[prop] != props2[prop]) { + console.debug(`Difference detected in ${prop}:\n\t1: ${props1[prop]}\n\t2: ${props2[prop]}`) + return false + } + } + return true + } + + createDateTimeProperty(propertyName, dateObject = new Date()) { + let property = new ICAL.Property(propertyName) + property.setParameter('tzid', 'America/Toronto') + property.setValue(ICAL.Time.fromJSDate(dateObject)) + + return property + } + + createCalendar(summary = 'test event', uid = 'test', transp = 'OPAQUE') { + const vcalendar = new ICAL.Component('vcalendar') + const vevent = new ICAL.Component('vevent') + const now = new Date() + const later = new Date(now.getTime() + 1000*60*60) // event lasts one hour + + vcalendar.addSubcomponent(vevent) + vevent.addPropertyWithValue('uid', uid) + vevent.addPropertyWithValue('summary', summary) + vevent.addPropertyWithValue('transp', transp) + vevent.addProperty(this.createDateTimeProperty('dtstart', now)) + vevent.addProperty(this.createDateTimeProperty('dtend', later)) + vevent.addProperty(this.createDateTimeProperty('dtstamp', now)) + vevent.addProperty(this.createDateTimeProperty('last-modified', now)) + vevent.addProperty(this.createDateTimeProperty('created', now)) + vevent.addPropertyWithValue('class', 'PUBLIC') + vevent.addPropertyWithValue('sequence', '0') + + return vcalendar + } + +} + +export default TestUtility \ No newline at end of file diff --git a/Tests/package.json b/Tests/package.json new file mode 100644 index 000000000..65cf59fb7 --- /dev/null +++ b/Tests/package.json @@ -0,0 +1,19 @@ +{ + "name": "tests", + "description": "This directory holds automated tests for SOGo.", + "devDependencies": {}, + "scripts": { + "test": "jasmine" + }, + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/preset-env": "^7.16.0", + "babel-cli": "^6.26.0", + "cookie": "^0.4.1", + "cross-fetch": "^3.1.4", + "esm": "^3.2.25", + "ical.js": "^1.4.0", + "jasmine": "^3.10.0", + "tsdav": "^1.1.5" + } +} diff --git a/Tests/spec/CalDAVPropertiesSpec.js b/Tests/spec/CalDAVPropertiesSpec.js new file mode 100644 index 000000000..22ab3cfda --- /dev/null +++ b/Tests/spec/CalDAVPropertiesSpec.js @@ -0,0 +1,61 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' + +describe('read and set calendar properties', function() { + const webdav = new WebDAV(config.username, config.password) + const resource = `/SOGo/dav/${config.username}/Calendar/test-dav-properties/` + + beforeEach(async function() { + await webdav.makeCalendar(resource) + }) + + afterEach(async function() { + await webdav.deleteObject(resource) + }) + + // CalDAVPropertiesTest + + it("propfind", async function() { + const [result] = await webdav.propfindCaldav(resource, ['schedule-calendar-transp']) + const { raw: { multistatus: { response: { propstat: { status, prop }}}}} = result + expect(status) + .withContext('schedule-calendar-transp profind is successful') + .toBe('HTTP/1.1 200 OK') + expect(Object.keys(prop).length) + .withContext('schedule-calendar-transp has one element only') + .toBe(1) + expect(Object.keys(prop.scheduleCalendarTransp).includes('opaque')) + .withContext('schedule-calendar-transp is "opaque" on new') + .toBeTrue() + }) + + it("proppatch", async function() { + let newValueNode + let results + + newValueNode = { 'thisvaluedoesnotexist': {} } + results = await webdav.proppatchCaldav(resource, {'schedule-calendar-transp': newValueNode}) + expect(results.length) + .toBe(1) + expect(results[0].status) + .withContext('Setting an invalid transparency is refused') + .toBe(400) + + newValueNode = { 'transparent': {} } + results = await webdav.proppatchCaldav(resource, {'schedule-calendar-transp': newValueNode}) + expect(results.length) + .toBe(1) + expect(results[0].status) + .withContext(`Setting transparency to ${newValueNode} is successful`) + .toBe(207) + + newValueNode = { 'opaque': {} } + results = await webdav.proppatchCaldav(resource, {'schedule-calendar-transp': newValueNode}) + expect(results.length) + .toBe(1) + expect(results[0].status) + .withContext(`Setting transparency to ${newValueNode} is successful`) + .toBe(207) + + }) +}) \ No newline at end of file diff --git a/Tests/spec/CardDAVSpec.js b/Tests/spec/CardDAVSpec.js new file mode 100644 index 000000000..44b0ae6fe --- /dev/null +++ b/Tests/spec/CardDAVSpec.js @@ -0,0 +1,181 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +import { + DAVNamespace, + DAVNamespaceShorthandMap, + davRequest, + formatProps, + getDAVAttribute, + propfind +} from 'tsdav' + +const cards = { + 'new.vcf': `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Inverse//Card Generator//EN +UID:NEWTESTCARD +N:New;Carte +FN:Carte 'new' +ORG:societe;service +NICKNAME:surnom +ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc +ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso +TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=home:tel dom +TEL;TYPE=cell:portable +TEL;TYPE=fax:fax +TEL;TYPE=pager:pager +X-MOZILLA-HTML:FALSE +EMAIL;TYPE=work:address.email@domaine.ca +EMAIL;TYPE=home:address.email@domaine2.com +URL;TYPE=home:web perso +TITLE:fonction +URL;TYPE=work:page soc +CUSTOM1:divers1 +CUSTOM2:divers2 +CUSTOM3:divers3 +CUSTOM4:divers4 +NOTE:Remarque +X-AIM:pseudo aim +END:VCARD`, + 'new-modified.vcf': `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Inverse//Card Generator//EN +UID:NEWTESTCARD +N:New;Carte modifiee +FN:Carte modifiee 'new' +ORG:societe;service +NICKNAME:surnom +ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc +ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso +TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=home:tel dom +TEL;TYPE=cell:portable +TEL;TYPE=fax:fax +TEL;TYPE=pager:pager +X-MOZILLA-HTML:FALSE +EMAIL;TYPE=work:address.email@domaine.ca +EMAIL;TYPE=home:address.email@domaine2.com +URL;TYPE=home:web perso +TITLE:fonction +URL;TYPE=work:page soc +CUSTOM1:divers1 +CUSTOM2:divers2 +CUSTOM3:divers3 +CUSTOM4:divers4 +NOTE:Remarque +X-AIM:pseudo aim +END:VCARD` +} + +describe('CardDAV extensions', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + const utility = new TestUtility(webdav) + + const resource = `/SOGo/dav/${config.username}/Contacts/test-carddav/` + + const _putCard = async function(client, filename, expectedCode, realCard) { + const card = cards[realCard || filename] + if (!card) + throw new Error(`Card ${realCard || filename} is unknown`) + const response = await client.createVCard(resource, filename, card) + expect(response.status).toBe(expectedCode) + } + + beforeAll(async function() { + await webdav.deleteObject(resource) + await webdav.makeCollection(resource) + for (let key of Object.keys(cards)) { + await _putCard(webdav, key, 201) + } + }) + + afterAll(async function() { + await webdav_su.deleteObject(resource) + }) + + // https://datatracker.ietf.org/doc/html/rfc6352#section-8.7 + it("supports for addressbook-multiget", async function() { + const hrefs = Object.keys(cards).map(c => `${resource}${c}`) + const response = await davRequest({ + url: webdav.serverUrl + resource, + init: { + method: 'REPORT', + namespace: DAVNamespaceShorthandMap[DAVNamespace.CARDDAV], + headers: { ...webdav.headers, depth: '0' }, + body: { + 'addressbook-multiget': { + _attributes: getDAVAttribute([ + DAVNamespace.CARDDAV, + DAVNamespace.DAV + ]), + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'address-data', namespace: DAVNamespace.CARDDAV }]), + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:href`]: hrefs + } + } + }, + }) + expect(response.length).toBe(2) + for (let r of response) { + const [name, ...rest] = r.href.split('/').reverse() + expect(r.status) + .withContext(`HTTP status code of addressbook-multiget`) + .toEqual(207) + expect(utility.componentsAreEqual(r.props.addressData, cards[name])) + .withContext(`Cards returned in addressbook-multiget`) + .toBe(true) + } + }) + + // https://datatracker.ietf.org/doc/html/rfc6352#section-8.7 + it("supports for propfind on global addressbook", async function() { + const hrefs = Object.keys(cards).map(c => `${resource}${c}`) + const response = await webdav.propfindWebdav(resource, ['getetag'], DAVNamespace.DAV, { depth: '1'}) + }) + + it("TEST", async function() { + // const response = await webdav_su.propfindWebdav(`/SOGo/dav/${config.superuser}/Contacts/personal/66AF-61E87800-1-3FD13600.vcf`, ['getetag', 'displayname'], DAVNamespace.DAV, { depth: '1'}) + // const response = await davRequest({ + // url: webdav_su.serverUrl + `/SOGo/dav/${config.superuser}/Contacts/personal/`, + // init: { + // method: 'REPORT', + // namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + // headers: webdav_su.headers, + // body: { + // 'addressbook-multiget': { + // _attributes: getDAVAttribute([ + // DAVNamespace.DAV + // ]), + // [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'displayname', namespace: DAVNamespace.DAV }]), + // } + // } + // }, + // }) + // const hrefs = [`/SOGo/dav/${config.superuser}/Contacts/personal/66AF-61E87800-1-3FD13600.vcf`] + // const response = await davRequest({ + // url: webdav_su.serverUrl + `/SOGo/dav/${config.superuser}/Contacts/personal/`, + // init: { + // method: 'REPORT', + // namespace: DAVNamespaceShorthandMap[DAVNamespace.CARDDAV], + // headers: { ...webdav_su.headers, depth: '0' }, + // body: { + // 'addressbook-multiget': { + // _attributes: getDAVAttribute([ + // DAVNamespace.CARDDAV, + // DAVNamespace.DAV + // ]), + // [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([ + // { name: 'getetag', namespace: DAVNamespace.DAV }, + // { name: 'displayname', namespace: DAVNamespace.CARDDAV }]), + // [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:href`]: hrefs + // } + // } + // }, + // }) + // const response = await webdav_su.propfindWebdav(`/SOGo/dav/${config.superuser}/Contacts/personal/66AF-61E87800-1-3FD13600.vcf`, [], DAVNamespace.DAV, { depth: '1'}) + // console.debug(response) + }) +}) \ No newline at end of file diff --git a/Tests/spec/ConfigSpec.js b/Tests/spec/ConfigSpec.js new file mode 100644 index 000000000..419da043d --- /dev/null +++ b/Tests/spec/ConfigSpec.js @@ -0,0 +1,38 @@ +import config from '../lib/config' + +describe('config tests', function() { + + it('required configuration parameters', async function() { + expect(config.hostname) + .withContext(`Config 'hostname'`) + .toBeDefined() + expect(config.username) + .withContext(`Config 'username'`) + .toBeDefined() + expect(config.subscriber_username) + .withContext(`Config 'subscriber_username'`) + .toBeDefined() + expect(config.attendee1) + .withContext(`Config 'attendee1'`) + .toBeDefined() + expect(config.attendee1_delegate) + .withContext(`Config 'attendee1_delegate'`) + .toBeDefined() + expect(config.mailserver) + .withContext(`Config 'mailserver'`) + .toBeDefined() + + expect(config.subscriber_username) + .withContext(`Config 'subscriber_username' and 'attendee1_username'`) + .toEqual(config.attendee1_username) + + let userHash = {} + const userList = [config.username, config.subscriber_username, config.attendee1_delegate_username] + for (let user of userList) { + expect(userHash[user]) + .withContext(`username, subscriber_username, attendee1_delegate_username must all be different users ('${user}')`) + .toBeUndefined() + userHash[user] = true + } + }) +}) \ No newline at end of file diff --git a/Tests/spec/DAVAddressBookAclSpec.js b/Tests/spec/DAVAddressBookAclSpec.js new file mode 100644 index 000000000..65b09fc9c --- /dev/null +++ b/Tests/spec/DAVAddressBookAclSpec.js @@ -0,0 +1,242 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +describe('create, read, modify, delete tasks for regular user', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + const webdav_subscriber = new WebDAV(config.subscriber_username, config.subscriber_password) + const utility = new TestUtility(webdav) + + const cards = { + 'new.vcf': `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Inverse//Card Generator//EN +UID:NEWTESTCARD +N:New;Carte +FN:Carte 'new' +ORG:societe;service +NICKNAME:surnom +ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc +ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso +TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=home:tel dom +TEL;TYPE=cell:portable +TEL;TYPE=fax:fax +TEL;TYPE=pager:pager +X-MOZILLA-HTML:FALSE +EMAIL;TYPE=work:address.email@domaine.ca +EMAIL;TYPE=home:address.email@domaine2.com +URL;TYPE=home:web perso +TITLE:fonction +URL;TYPE=work:page soc +CUSTOM1:divers1 +CUSTOM2:divers2 +CUSTOM3:divers3 +CUSTOM4:divers4 +NOTE:Remarque +X-AIM:pseudo aim +END:VCARD`, + 'old.vcf': `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Inverse//Card Generator//EN +UID:NEWTESTCARD +N:Old;Carte +FN:Carte 'old' +ORG:societe;service +NICKNAME:surnom +ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc +ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso +TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=home:tel dom +TEL;TYPE=cell:portable +TEL;TYPE=fax:fax +TEL;TYPE=pager:pager +X-MOZILLA-HTML:FALSE +EMAIL;TYPE=work:address.email@domaine.ca +EMAIL;TYPE=home:address.email@domaine2.com +URL;TYPE=home:web perso +TITLE:fonction +URL;TYPE=work:page soc +CUSTOM1:divers1 +CUSTOM2:divers2 +CUSTOM3:divers3 +CUSTOM4:divers4 +NOTE:Remarque +X-AIM:pseudo aim +END:VCARD`, + 'new-modified.vcf': `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Inverse//Card Generator//EN +UID:NEWTESTCARD +N:New;Carte modifiee +FN:Carte modifiee 'new' +ORG:societe;service +NICKNAME:surnom +ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc +ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso +TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=home:tel dom +TEL;TYPE=cell:portable +TEL;TYPE=fax:fax +TEL;TYPE=pager:pager +X-MOZILLA-HTML:FALSE +EMAIL;TYPE=work:address.email@domaine.ca +EMAIL;TYPE=home:address.email@domaine2.com +URL;TYPE=home:web perso +TITLE:fonction +URL;TYPE=work:page soc +CUSTOM1:divers1 +CUSTOM2:divers2 +CUSTOM3:divers3 +CUSTOM4:divers4 +NOTE:Remarque +X-AIM:pseudo aim +END:VCARD`, + 'old-modified.vcf': `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Inverse//Card Generator//EN +UID:NEWTESTCARD +N:Old;Carte modifiee +FN:Carte modifiee 'old' +ORG:societe;service +NICKNAME:surnom +ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc +ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso +TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=home:tel dom +TEL;TYPE=cell:portable +TEL;TYPE=fax:fax +TEL;TYPE=pager:pager +X-MOZILLA-HTML:FALSE +EMAIL;TYPE=work:address.email@domaine.ca +EMAIL;TYPE=home:address.email@domaine2.com +URL;TYPE=home:web perso +TITLE:fonction +URL;TYPE=work:page soc +CUSTOM1:divers1 +CUSTOM2:divers2 +CUSTOM3:divers3 +CUSTOM4:divers4 +NOTE:Remarque +X-AIM:pseudo aim +END:VCARD` + } + + const sogoRights = { + c: 'ObjectCreator', + d: 'ObjectEraser', + v: 'ObjectViewer', + e: 'ObjectEditor' + } + + const resource = `/SOGo/dav/${config.username}/Contacts/test-dav-acl/` + + const _putCard = async function(client, filename, expectedCode, realCard) { + const card = cards[realCard || filename] + if (!card) + throw new Error(`Card ${realCard || filename} is unknown`) + const response = await client.createVCard(resource, filename, card) + expect(response.status).toBe(expectedCode) + } + + const _getCard = async function(client, filename, expectedCode) { + const [{ status }] = await client.getCard(resource, filename) + expect(status).toBe(expectedCode) + } + + const _deleteCard = async function(client, filename, expectedCode = 204) { + const response = await client.deleteObject(resource + filename) + expect(response.status) + .withContext('HTTP status code when deleting a card') + .toBe(expectedCode) + } + + const _testView = async function(rights) { + let expectedCode = 403 + if (rights && (rights.v || rights.e)) { + expectedCode = 200 + } + await _getCard(webdav_subscriber, 'old.vcf', expectedCode) + } + + const _testCreate = async function(rights) { + let expectedCode + if (rights && rights.c) + expectedCode = 201 + else + expectedCode = 403 + await _putCard(webdav_subscriber, 'new.vcf', expectedCode) + } + + const _testModify = async function(rights) { + let expectedCode + if (rights && rights.e) + expectedCode = 204 + else + expectedCode = 403 + await _putCard(webdav_subscriber, 'old.vcf', expectedCode, 'old-modified.vcf') + } + + const _testDelete = async function(rights) { + let expectedCode = 403 + if (rights && rights.d) { + expectedCode = 204 + } + await _deleteCard(webdav_subscriber, 'old.vcf', expectedCode) + } + + const _testRights = async function(rights) { + const results = await utility.setupAddressBookRights(resource, config.subscriber_username, rights) + expect(results.length).toBe(1) + expect(results[0].status).toBe(204) + await _testCreate(rights) + await _testView(rights) + await _testModify(rights) + await _testDelete(rights) + } + + beforeEach(async function() { + await webdav.deleteObject(resource) + await webdav.makeCollection(resource) + await _putCard(webdav, 'old.vcf', 201) + }) + + afterEach(async function() { + await webdav_su.deleteObject(resource) + }) + + // DAVAddressBookAclTest + + it("'view' only", async function() { + await _testRights({ v: true }) + }) + + it("'edit' only", async function() { + await _testRights({ e: true }) + }) + + it("'create' only", async function() { + await _testRights({ c: true }) + }) + + it("'delete' only", async function() { + await _testRights({ d: true }) + }) + + it("'create', 'delete'", async function() { + await _testRights({ c: true, d: true }) + }) + + it("'view', 'delete'", async function() { + await _testRights({ v: true, d: true }) + }) + + it("'edit', 'create'", async function() { + await _testRights({ c: true, e: true }) + }) + + it("'edit', 'delete'", async function() { + await _testRights({ d: true, e: true }) + }) +}) \ No newline at end of file diff --git a/Tests/spec/DAVCalendarAclSpec.js b/Tests/spec/DAVCalendarAclSpec.js new file mode 100644 index 000000000..f5ee52a45 --- /dev/null +++ b/Tests/spec/DAVCalendarAclSpec.js @@ -0,0 +1,452 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +describe('create, read, modify, delete events for regular user', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + const webdav_subscriber = new WebDAV(config.subscriber_username, config.subscriber_password) + const utility = new TestUtility(webdav) + + const event_template = `BEGIN:VCALENDAR +PRODID:-//Inverse//Event Generator//EN +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:0 +TRANSP:OPAQUE +UID:12345-%(class)-%(filename) +SUMMARY:%(class) event (orig. title) +DTSTART:20090805T100000Z +DTEND:20090805T140000Z +CLASS:%(class) +DESCRIPTION:%(class) description +LOCATION:location +%(organizer_line)%(attendee_line)CREATED:20090805T100000Z +DTSTAMP:20090805T100000Z +END:VEVENT +END:VCALENDAR` + + const task_template = `BEGIN:VCALENDAR +PRODID:-//Inverse//Event Generator//EN +VERSION:2.0 +BEGIN:VTODO +CREATED:20100122T201440Z +LAST-MODIFIED:20100201T175246Z +DTSTAMP:20100201T175246Z +UID:12345-%(class)-%(filename) +SUMMARY:%(class) event (orig. title) +CLASS:%(class) +DESCRIPTION:%(class) description +STATUS:IN-PROCESS +PERCENT-COMPLETE:0 +END:VTODO +END:VCALENDAR` + + const resource = `/SOGo/dav/${config.username}/Calendar/test-dav-acl/` + const classToICSClass = { + 'pu': 'PUBLIC', + 'pr': 'PRIVATE', + 'co': 'CONFIDENTIAL' + } + + let user + + const _checkViewEventRight = function(operation, event, eventClass, right) { + if (right) { + expect(event) + .withContext(`Returned event during operation '${operation}'`) + .toBeTruthy() + if (['v', 'r', 'm'].includes(right)) { + const iscClass = classToICSClass[eventClass] + const expectedEvent = utility.formatTemplate(event_template, { + 'class': iscClass, + 'filename': `${iscClass.toLowerCase()}-event.ics` + }) + expect(event).toBe(expectedEvent) + } + else if (right == 'd') { + _testEventIsSecureVersion(eventClass, event) + } + else { + throw new Error(`Right '${right} is not supported`) + } + } + else { + expect(event).toBeFalsy() + } + } + + const _currentUserPrivilegeSet = async function(resource, expectedCode = 207) { + const results = await webdav_subscriber.currentUserPrivilegeSet(resource) + expect(results.length).toBe(1) + const response = results[0] + expect(response.status).toBe(expectedCode) + let privileges = [] + if (expectedCode < 300) { + privileges = response.props.currentUserPrivilegeSet.privilege.map(o => { + return Object.keys(o)[0] + }) + } + return privileges + } + + const _deleteEvent = async function(client, filename, expectedCode = 204) { + const response = await client.deleteObject(resource + filename) + expect(response.status).toBe(expectedCode) + } + + const _getEvent = async function(eventClass, isInvitation = false) { + const iscClass = classToICSClass[eventClass].toLowerCase() + const filename = (isInvitation ? `invitation-${iscClass}` : iscClass) + '-event.ics' + const [{ status, raw = '' }] = await webdav_subscriber.getObject(resource, filename) + + if (status == 200) + return raw.replace(/\r\n/g,'\n') + else + return undefined + } + + const _multigetEvent = async function(eventClass) { + const iscClass = classToICSClass[eventClass].toLowerCase() + const filename = `${iscClass}-event.ics` + let event = undefined + const results = await webdav_subscriber.calendarMultiGet(resource, filename) + if (results.status !== 404) { + results.find(o => { + if (o.href == resource + filename) { + const { props: { calendarData = '' } } = o + event = calendarData.replace(/\r\n/g,'\n') + return true + } + return false + }) + } + return event + } + + const _propfindEvent = async function(eventClass) { + const iscClass = classToICSClass[eventClass].toLowerCase() + const filename = `${iscClass}-event.ics` + const results = await webdav_subscriber.propfindEvent(resource + filename) + let event = undefined + if (results.status !== 404) { + results.find(o => { + if (o.href == resource + filename) { + const { props: { calendarData = '' } } = o + event = calendarData.replace(/\r\n/g,'\n') + return true + } + return false + }) + } + return event + } + + const _putEvent = async function(client, filename, eventClass = 'PUBLIC', expectedCode = 201, organizer, attendee, partstat = 'NEEDS-ACTION') { + const organizer_line = organizer ? `ORGANIZER:${organizer}\n` : '' + const attendee_line = attendee ? `ATTENDEE;PARTSTAT=${partstat}:${attendee}\n` : '' + const event = utility.formatTemplate(event_template, { + 'class': eventClass, + 'filename': filename, + organizer_line, + attendee_line + }) + const response = await client.createCalendarObject(resource, filename, event) + expect(response.status).toBe(expectedCode) + } + + const _webdavSyncEvent = async function(eventClass) { + const iscClass = classToICSClass[eventClass].toLowerCase() + const filename = `${iscClass}-event.ics` + let event = undefined + const results = await webdav_subscriber.syncCollection(resource) + if (results.status !== 404) { + results.find(o => { + if (o.href == resource + filename) { + const { props: { calendarData = '' } } = o + event = calendarData.length ? calendarData.replace(/\r\n/g,'\n') : undefined + return true + } + return false + }) + } + return event + } + + const _testCreate = async function(rights) { + let expectedCode + if (rights.c) + expectedCode = 201 + else if (Object.keys(rights).length === 0) + expectedCode = 404 + else + expectedCode = 403 + return _putEvent(webdav_subscriber, 'creation-test.ics', 'PUBLIC', expectedCode) + } + + const _testCollectionDAVAcl = async function(rights) { + let expectedPrivileges = [] + if (Object.keys(rights).length > 0) { + expectedPrivileges.push('read', 'readCurrentUserPrivilegeSet', 'readFreeBusy') + } + if (rights.c) { + expectedPrivileges.push( + 'bind', + 'writeContent', + 'schedule', + 'schedulePost', + 'schedulePostVevent', + 'schedulePostVtodo', + 'schedulePostVjournal', + 'schedulePostVfreebusy', + 'scheduleDeliver', + 'scheduleDeliverVevent', + 'scheduleDeliverVtodo', + 'scheduleDeliverVjournal', + 'scheduleDeliverVfreebusy', + 'scheduleRespond', + 'scheduleRespondVevent', + 'scheduleRespondVtodo' + ) + } + if (rights.d) { + expectedPrivileges.push('unbind') + } + const expectedCode = (expectedPrivileges.length == 0) ? 404 : 207 + const privileges = await _currentUserPrivilegeSet(resource, expectedCode) + + // When comparing privileges on DAV collection, we remove all 'default' + // privileges on the collection. + for (const c of ['Public', 'Private', 'Confidential']) { + for (const r of ['viewdant', 'viewwhole', 'modify', 'respondto']) { + const i = privileges.indexOf(`${r}${c}Records`) + if (i >= 0) { + privileges.splice(i, 1) + } + } + } + // for (const privilege of ['read', 'readCurrentUserPrivilegeSet', 'readFreeBusy']) { + for (const expectedPrivilege of expectedPrivileges) { + expect(privileges).toContain(expectedPrivilege) + } + } + + const _testEventIsSecureVersion = function(eventClass, event) { + const iscClass = classToICSClass[eventClass].toLowerCase().replace(/^\w/, c => c.toUpperCase()) + const expectedDict = { + version: 'VERSION:2.0', + prodid: 'PRODID:-//Inverse//Event Generator//EN', + summary: `SUMMARY:(${iscClass} event)`, + dtstart: 'DTSTART:20090805T100000Z', + dtend: 'DTEND:20090805T140000Z', + dtstamp: 'DTSTAMP:20090805T100000Z', + 'x-sogo-secure': 'X-SOGO-SECURE:YES' + } + const eventDict = utility.versitDict(event) + // Ignore UID + for (const key of Object.keys(eventDict).filter(k => k !== 'uid')) { + expect(expectedDict[key]) + .withContext(`Key ${key} of secure event is expected`) + .toBeTruthy() + if (expectedDict[key]) + expect(expectedDict[key]) + .withContext(`Value of key ${key} of secure event is valid`) + .toBe(eventDict[key]) + } + for (const key of Object.keys(expectedDict)) { + expect(eventDict[key]) + .withContext(`Key ${key} of secure event is present`) + .toBeTruthy() + } + } + + const _testModify = async function(eventClass, right, errorCode) { + const iscClass = classToICSClass[eventClass] + const filename = `${iscClass.toLowerCase()}-event.ics` + let expectedCode = errorCode + if (['r', 'm'].includes(right)) + expectedCode = 204 + return _putEvent(webdav_subscriber, filename, iscClass, expectedCode) + } + + const _testRespondTo = async function(eventClass, right, errorCode) { + const iscClass = classToICSClass[eventClass] + const filename = `invitation-${iscClass.toLowerCase()}-event.ics` + let expectedCode = errorCode + if (['r', 'm'].includes(right)) + expectedCode = 204 + + await _putEvent(webdav, filename, iscClass, 201, 'mailto:nobody@somewhere.com', user.email, 'NEEDS-ACTION') + + // here we only do 'passive' validation: if a user has a "respond to" + // right, only the attendee entry will me modified. The change of + // organizer must thus be silently ignored below. + await _putEvent(webdav_subscriber, filename, iscClass, expectedCode, 'mailto:someone@nowhere.com', user.email, 'ACCEPTED') + + if (expectedCode == 204) { + const attendee_line = `ATTENDEE;PARTSTAT=ACCEPTED:${user.email}\n` + let expectedEvent + if (right == 'r') { + expectedEvent = utility.formatTemplate(event_template, { + 'class': iscClass, + 'filename': filename, + organizer_line: 'ORGANIZER;CN=nobody@somewhere.com:mailto:nobody@somewhere.com\n', + attendee_line + }) + } + else { + expectedEvent = utility.formatTemplate(event_template, { + 'class': iscClass, + 'filename': filename, + organizer_line: 'ORGANIZER;CN=someone@nowhere.com:mailto:someone@nowhere.com\n', + attendee_line + }) + } + const event = await _getEvent(eventClass, true) + expect(utility.componentsAreEqual(expectedEvent, event)) + .withContext('Calendars of organizer and attendee are identical') + .toBe(true) + } + } + + const _testEventDAVAcl = async function(eventClass, right, errorCode) { + const iscClass = classToICSClass[eventClass].toLowerCase() + for (const suffix of ['event', 'task']) { + const filename = `${iscClass}-${suffix}.ics` + let expectedCode = errorCode + let expectedPrivileges = [] + if (right) { + expectedCode = 207 + expectedPrivileges.push('readCurrentUserPrivilegeSet', 'viewDateAndTime', 'read') + if (right != 'd') { + expectedPrivileges.push('viewWholeComponent') + if (right != 'v') { + expectedPrivileges.push('respondToComponent', 'writeContent') + if (right != 'r') { + expectedPrivileges.push('writeProperties', 'write') + } + } + } + } + const privileges = await _currentUserPrivilegeSet(resource + filename, expectedCode) + if (errorCode != expectedCode) { + for (const expectedPrivilege of expectedPrivileges) { + expect(privileges).toContain(expectedPrivilege) + } + } + } + } + + const _testEventRight = async function(eventClass, rights) { + const right = Object.keys(rights).includes(eventClass) ? rights[eventClass] : undefined + + let event + + event = await _getEvent(eventClass) + _checkViewEventRight('GET', event, eventClass, right) + + event = await _propfindEvent(eventClass) + _checkViewEventRight('PROPFIND', event, eventClass, right) + + event = await _multigetEvent(eventClass) + _checkViewEventRight('multiget', event, eventClass, right) + + event = await _webdavSyncEvent(eventClass) + _checkViewEventRight('webdav-sync', event, eventClass, right) + + const errorCode = (Object.keys(rights).length > 0) ? 403 : 404 + await _testModify(eventClass, right, errorCode) + await _testRespondTo(eventClass, right, errorCode) + await _testEventDAVAcl(eventClass, right, errorCode) + } + + const _testDelete = async function(rights) { + let expectedCode = 403 + if (rights && rights.d) { + expectedCode = 204 + } + else if (Object.keys(rights) == 0) { + expectedCode = 404 + } + for (const eventClass of Object.values(classToICSClass)) { + await _deleteEvent(webdav_subscriber, `${eventClass.toLocaleLowerCase()}-event.ics`, expectedCode) + } + } + + const _testRights = async function(rights) { + const results = await utility.setupCalendarRights(resource, config.subscriber_username, rights) + expect(results.length).toBe(1) + expect(results[0].status) + .withContext(`Setup rights (${JSON.stringify(rights)}) on ${resource}`) + .toBe(204) + await _testCreate(rights) + await _testCollectionDAVAcl(rights) + await _testEventRight('pu', rights) + await _testEventRight('pr', rights) + await _testEventRight('co', rights) + await _testDelete(rights) + } + + beforeEach(async function() { + user = await utility.fetchUserInfo(config.username) + await webdav.deleteObject(resource) + await webdav.makeCalendar(resource) + for (const c of Object.values(classToICSClass)) { + // Create event for each class + const eventFilename = `${c.toLowerCase()}-event.ics` + const event = utility.formatTemplate(event_template, { + 'class': c, + 'filename': eventFilename + }) + let response = await webdav.createCalendarObject(resource, eventFilename, event) + expect(response.status) + .withContext(`HTTP status when creating event with ${c} class`) + .toBe(201) + // Create task for each class + const taskFilename = `${c.toLowerCase()}-task.ics` + const task = utility.formatTemplate(task_template, { + 'class': c, + 'filename': taskFilename + }) + response = await webdav.createCalendarObject(resource, taskFilename, task) + expect(response.status) + .withContext(`HTTP status when creating task with ${c} class`) + .toBe(201) + } + }) + + afterEach(async function() { + await webdav_su.deleteObject(resource) + }) + + // DAVCalendarAclTest + + it("'view all' on a specific class (PUBLIC)", async function() { + await _testRights({ pu: 'v' }) + }) + + it("'modify' PUBLIC, 'view all' PRIVATE, 'view d&t' confidential", async function() { + await _testRights({ pu: 'm', pr: 'v', co: 'd' }) + }) + + it("'create' only", async function() { + await _testRights({ c: true }) + }) + + it("'delete' only", async function() { + await _testRights({ d: true }) + }) + + it("'create', 'delete', 'view d&t' PUBLIC, 'modify' PRIVATE", async function() { + await _testRights({ c: true, d: true, pu: 'd', pr: 'm' }) + }) + + it("'create', 'respond to' PUBLIC", async function() { + await _testRights({ c: true, pu: 'r' }) + }) + + it("no right given", async function() { + await _testRights({}) + }) + +}) \ No newline at end of file diff --git a/Tests/spec/DAVCalendarAppleiCalSpec.js b/Tests/spec/DAVCalendarAppleiCalSpec.js new file mode 100644 index 000000000..46b28fa1b --- /dev/null +++ b/Tests/spec/DAVCalendarAppleiCalSpec.js @@ -0,0 +1,229 @@ +import { DAVNamespace } from 'tsdav' +import config from '../lib/config' +import { default as WebDAV, DAVInverse } from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +/** + * NOTE + * + * To pass the following tests, make sure "username" and "subscriber_username" don't have + * additional calendars. + */ + +describe('Apple iCal', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + const utility = new TestUtility(webdav_su) + + const iCal4UserAgent = 'DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)' + + const _setMemberSet = async function(owner, members, perm) { + const resource = `/SOGo/dav/${owner}/calendar-proxy-${perm}` + const headers = { 'User-Agent': iCal4UserAgent } + const membersHref = members.map(m => { + return `/SOGo/dav/${m}` + }) + const properties = { + 'group-member-set': membersHref.length ? { href: membersHref } : '' + } + const results = await webdav_su.proppatchWebdav(resource, properties, DAVNamespace.DAV, headers) + + expect(results.length) + .withContext(`Number of responses from PROPPATCH on group-member-set for ${owner}`) + .toBe(1) + expect(results[0].status) + .withContext(`HTTP status code when setting group member on calendar-proxy-${perm} for ${owner}`) + .toBe(207) + } + + const _getMembership = async function(user) { + const resource = `/SOGo/dav/${user}/` + const headers = { 'User-Agent': iCal4UserAgent } + const results = await webdav_su.propfindWebdav(resource, ['group-membership'], DAVNamespace.DAV, headers) + + expect(results.length) + .withContext(`Number of responses from PROPFIND on group-membership for ${user}`) + .toBe(1) + expect(results[0].status) + .withContext(`HTTP status code when getting group membership for ${user}`) + .toBe(207) + + const { props: { groupMembership: { href = [] } = {} } = {} } = results[0] + + return Array.isArray(href) ? href : [href] // always return an array + } + + const _getProxyFor = async function(user, perm) { + const resource = `/SOGo/dav/${user}/` + const headers = { 'User-Agent': iCal4UserAgent } + const results = await webdav_su.propfindWebdav(resource, [`calendar-proxy-${perm}-for`], DAVNamespace.CALENDAR_SERVER, headers) + + expect(results.length) + .withContext(`Number of responses from PROPFIND on group-membership for ${user}`) + .toBe(1) + expect(results[0].status) + .withContext(`HTTP status code when getting group membership for ${user}`) + .toBe(207) + + const { props = {} } = results[0] + const users = props[`calendarProxy${perm.replace(/^\w/, (c) => c.toUpperCase())}For`] + const { href = [] } = users + + return Array.isArray(href) ? href : [href] // always return an array + } + + const _testMapping = async function(perm, resource, rights) { + const results = await utility.setupCalendarRights(resource, config.subscriber_username, rights) + expect(results.length).toBe(1) + expect(results[0].status).toBe(204) + + const membership = await _getMembership(config.subscriber_username) + expect(membership) + .withContext(`${perm.replace(/^\w/, (c) => c.toUpperCase())} access to /SOGo/dav/${config.subscriber_username}/`) + .toContain(`/SOGo/dav/${config.username}/calendar-proxy-${perm}/`) + + const proxyFor = await _getProxyFor(config.subscriber_username, perm) + expect(proxyFor) + .withContext(`Proxy ${perm} on /SOGo/dav/${config.subscriber_username}/`) + .toContain(`/SOGo/dav/${config.username}/`) + } + + // iCalTest + + it(`principal-collection-set: 'DAV' header must be returned with iCal 4`, async function() { + const resource = `/SOGo/dav/${config.username}/` + const expectedDAVClasses = ['1', '2', 'access-control', 'calendar-access', 'calendar-schedule', 'calendar-auto-schedule', 'calendar-proxy'] + + let headers, response, davClasses, davClass + headers = { Depth: new String(0) } + + // NOT iCal4 + response = await webdav.propfindWebdavRaw(resource, ['principal-collection-set'], headers) + expect(response.status) + .withContext(`HTTP status code when fetching principal-collection-set`) + .toBe(207) + expect(response.headers.get('dav')) + .withContext(`DAV header must NOT be returned when user-agent is NOT iCal 4`) + .toBeFalsy() + + // iCal4 + headers['User-Agent'] = iCal4UserAgent + response = await webdav.propfindWebdavRaw(resource, ['principal-collection-set'], headers) + expect(response.status) + .withContext(`HTTP status code when fetching principal-collection-set`) + .toBe(207) + expect(response.headers.get('dav')) + .withContext(`DAV header must be returned when user-agent is iCal 4`) + .toBeTruthy() + + davClasses = response.headers.get('dav').split(', ') + for (davClass of expectedDAVClasses) { + expect(davClasses.includes(davClass)) + .withContext(`DAV header includes class ${davClass}`) + .toBeTrue() + } + }) + + it(`calendar-proxy as used from iCal`, async function() { + let membership, perm, users, proxyFor + + await _setMemberSet(config.username, [], 'read') + await _setMemberSet(config.username, [], 'write') + await _setMemberSet(config.subscriber_username, [], 'read') + await _setMemberSet(config.subscriber_username, [], 'write') + + membership = await _getMembership(config.username) + expect(membership.length) + .toBe(0) + membership = await _getMembership(config.subscriber_username) + expect(membership.length) + .toBe(0) + + users = await _getProxyFor(config.username, 'read') + expect(users.length) + .withContext(`Proxy read for /SOGo/dav/${config.username}`) + .toBe(0) + users = await _getProxyFor(config.username, 'write') + expect(users.length) + .withContext(`Proxy write for /SOGo/dav/${config.username}`) + .toBe(0) + users = await _getProxyFor(config.subscriber_username, 'read') + expect(users.length) + .withContext(`Proxy read for /SOGo/dav/${config.subscriber_username}`) + .toBe(0) + users = await _getProxyFor(config.subscriber_username, 'write') + expect(users.length) + .withContext(`Proxy write for /SOGo/dav/${config.subscriber_username}`) + .toBe(0) + + for (perm of ['read', 'write']) { + for (users of [[config.username, config.subscriber_username], [config.subscriber_username, config.username]]) { + const [owner, member] = users + + await _setMemberSet(owner, [member], perm) + + let [ membership ] = await _getMembership(member) + expect(membership) + .toBe(`/SOGo/dav/${owner}/calendar-proxy-${perm}/`) + + proxyFor = await _getProxyFor(member, perm) + expect(proxyFor.length).toBe(1) + expect(proxyFor).toContain(`/SOGo/dav/${owner}/`) + } + } + }) + + it('calendar-proxy as used from SOGo', async function() { + const personalResource = `/SOGo/dav/${config.username}/Calendar/personal/` + const otherResource = `/SOGo/dav/${config.username}/Calendar/test-calendar-proxy2/` + + let response, membership + + // Remove rights on personal calendar + await utility.setupRights(personalResource, config.subscriber_username); + [response] = await utility.subscribe(personalResource, [config.subscriber_username]) + expect(response.status) + .toBe(200) + + await webdav_su.deleteObject(otherResource) + await webdav_su.makeCalendar(otherResource) + await utility.setupRights(otherResource, config.subscriber_username); + [response] = await utility.subscribe(otherResource, [config.subscriber_username]) + expect(response.status) + .toBe(200) + + // we test the rights mapping + // write: write on 'personal', none on 'test-calendar-proxy2' + await _testMapping('write', personalResource, { c: true, d: false, pu: 'v' }) + await _testMapping('write', personalResource, { c: false, d: true, pu: 'v' }) + await _testMapping('write', personalResource, { c: false, d: false, pu: 'm' }) + await _testMapping('write', personalResource, { c: false, d: false, pu: 'r' }) + + // read: read on 'personal', none on 'test-calendar-proxy2' + await _testMapping('read', personalResource, { c: false, d: false, pu: 'd' }) + await _testMapping('read', personalResource, { c: false, d: false, pu: 'v' }) + + // write: read on 'personal', write on 'test-calendar-proxy2' + await _testMapping('write', otherResource, { c: false, d: false, pu: 'r' }); + + // we test the unsubscription + // unsubscribed from personal, subscribed to 'test-calendar-proxy2' + [response] = await utility.unsubscribe(personalResource, [config.subscriber_username]) + expect(response.status) + .toBe(200) + membership = await _getMembership(config.subscriber_username) + expect(membership) + .withContext(`Proxy write to /SOGo/dav/${config.subscriber_username}/`) + .toContain(`/SOGo/dav/${config.username}/calendar-proxy-write/`); + // unsubscribed from personal, unsubscribed from 'test-calendar-proxy2' + [response] = await utility.unsubscribe(otherResource, [config.subscriber_username]) + expect(response.status) + .toBe(200) + membership = await _getMembership(config.subscriber_username) + expect(membership.length) + .withContext(`No more access to /SOGo/dav/${config.subscriber_username}/`) + .toBe(0) + + await webdav_su.deleteObject(otherResource) + }) +}) \ No newline at end of file diff --git a/Tests/spec/DAVCalendarClassificationSpec.js b/Tests/spec/DAVCalendarClassificationSpec.js new file mode 100644 index 000000000..ca72dfc19 --- /dev/null +++ b/Tests/spec/DAVCalendarClassificationSpec.js @@ -0,0 +1,50 @@ +import config from '../lib/config' +import { default as WebDAV, DAVInverse } from '../lib/WebDAV' + +describe('calendar classification', function() { + const webdav = new WebDAV(config.username, config.password) + + const _setClassification = async function(component, classification = '') { + const resource = `/SOGo/dav/${config.username}/Calendar/` + const properties = { [`${component}-default-classification`]: classification } + + const results = await webdav.proppatchWebdav(resource, properties, DAVInverse) + expect(results.length) + .withContext(`Set ${component} classification to ${classification}`) + .toBe(1) + + return results[0].status + } + + // HTTPDefaultClassificationTest + + it('expected failure when setting a classification with an invalid property', async function() { + let status + + status = await _setClassification('123456', 'PUBLIC') + expect(status) + .withContext('Setting an invalid classification property') + .toBe(403) + + status = await _setClassification('events', '') + expect(status) + .withContext('Setting an empty classification') + .toBe(403) + + status = await _setClassification('events', 'pouet') + expect(status) + .withContext('Setting an invalid classification') + .toBe(403) + }) + + it('setting a valid classification', async function() { + for (let component of ['events', 'tasks']) { + for (let classification of ['PUBLIC', 'PRIVATE', 'CONFIDENTIAL']) { + const status = await _setClassification(component, classification) + expect(status) + .withContext(`Set ${component} classification to ${classification}`) + .toBe(207) + } + } + }) +}) \ No newline at end of file diff --git a/Tests/spec/DAVCalendarPublicAclSpec.js b/Tests/spec/DAVCalendarPublicAclSpec.js new file mode 100644 index 000000000..54607eaee --- /dev/null +++ b/Tests/spec/DAVCalendarPublicAclSpec.js @@ -0,0 +1,181 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +describe('public access', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_anon = new WebDAV() + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + const webdav_subscriber = new WebDAV(config.subscriber_username, config.subscriber_password) + const utility = new TestUtility(webdav) + const utility_subscriber = new TestUtility(webdav_subscriber) + let createdRsrc + + // DAVCalendarPublicAclTest + + afterEach(async function() { + if (createdRsrc) { + await webdav_su.deleteObject(createdRsrc) + } + }) + + it("normal user access to (non-)shared resource from su", async function() { + const parentColl = `/SOGo/dav/${config.username}/Calendar/` + let results + let href + + // 1. all rights removed + createdRsrc = `${parentColl}test-dav-acl/` + for (const rsrc of ['personal', 'test-dav-acl']) { + const resource = `${parentColl}${rsrc}/` + await webdav.makeCalendar(resource) + await utility.setupRights(resource, 'anonymous', {}) + await utility.setupRights(resource, config.subscriber_username, {}) + await utility.setupRights(resource, '', {}) + } + + results = await webdav_subscriber.propfindURL(parentColl) + expect(results.length) + .withContext(`Profind returns 1 href when subscriber user ${config.subscriber_username} has no right`) + .toBe(1) + href = results[0].href + expect(href) + .withContext(`Unique href must be the Calendar parent collection ${parentColl}`) + .toBe(parentColl) + + // 2. creation right added + await utility.setupCalendarRights(createdRsrc, config.subscriber_username, { c: true }) + + results = await webdav_subscriber.propfindURL(parentColl) + expect(results.length) + .withContext(`Profind returns 4 href when subscriber user ${config.subscriber_username} has creation right`) + .toBe(4) + href = results[0].href + expect(href) + .withContext(`First href must be the Calendar parent collection ${parentColl}`) + .toBe(parentColl) + + let resourceHrefs = { + [createdRsrc]: false, + [`${createdRsrc.slice(0, -1)}.xml`]: false, + [`${createdRsrc.slice(0, -1)}.ics`]: false + } + for (href of results.map(r => r.href).slice(1)) { + expect(Object.keys(resourceHrefs).includes(href)) + .withContext(`Propfind href ${href} is returned`) + .toBeTrue() + expect(resourceHrefs[href]) + .not.toBeTrue() + resourceHrefs[href] = true + } + + await utility.setupRights(createdRsrc, config.subscriber_username) // remove rights + + // 3. creation right added for "default user" + // subscriber_username expected to have access, but not "anonymous" + await utility.setupCalendarRights(createdRsrc, '', { c: true }) + + results = await webdav_subscriber.propfindURL(parentColl) + expect(results.length) + .withContext('Profind returns 4 href when user has creation right') + .toBe(4) + href = results[0].href + expect(href) + .withContext('First href must be the Calendar parent collection') + .toBe(parentColl) + + resourceHrefs = { + [createdRsrc]: false, + [`${createdRsrc.slice(0, -1)}.xml`]: false, + [`${createdRsrc.slice(0, -1)}.ics`]: false + } + for (href of results.map(r => r.href).slice(1)) { + expect(Object.keys(resourceHrefs).includes(href)) + .withContext(`Propfind href ${href} is returned`) + .toBeTrue() + expect(resourceHrefs[href]) + .withContext(`Propfind href ${href} is returned only once`) + .not.toBeTrue() + resourceHrefs[href] = true + } + + const anonParentColl = `/SOGo/dav/public/${config.username}/Calendar/` + results = await webdav_anon.propfindURL(anonParentColl) + expect(results.length) + .withContext('Profind returns 1 href for anonymous user') + .toBe(1) + href = results[0].href + expect(href) + .withContext('Unique href must be the Calendar parent collection') + .toBe(anonParentColl) + + await utility.setupRights(createdRsrc, '', {}) + + // 4. creation right added for "anonymous" + // "anonymous" expected to have access, but not subscriber_username + + await utility.setupCalendarRights(createdRsrc, 'anonymous', { c: true }) + + results = await webdav_anon.propfindURL(anonParentColl) + expect(results.length) + .withContext('Profind returns 4 href when anonymous user has creation right') + .toBe(4) + href = results[0].href + expect(href) + .withContext('First href must be the Calendar parent collection') + .toBe(anonParentColl) + + const anonRsrc = `${anonParentColl}test-dav-acl/` + resourceHrefs = { + [anonRsrc]: false, + [`${anonRsrc.slice(0, -1)}.xml`]: false, + [`${anonRsrc.slice(0, -1)}.ics`]: false + } + for (href of results.map(r => r.href).slice(1)) { + expect(Object.keys(resourceHrefs).includes(href)) + .withContext(`Propfind href ${href} is returned`) + .toBeTrue() + expect(resourceHrefs[href]) + .withContext(`Propfind href ${href} is returned only once`) + .not.toBeTrue() + resourceHrefs[href] = true + } + + results = await webdav_subscriber.propfindURL(parentColl) + expect(results.length) + .withContext('Profind returns 1 href when user has no right') + .toBe(1) + href = results[0].href + expect(href) + .withContext('First href must be the Calendar parent collection') + .toBe(parentColl) + + }) + + it("user accessing (non-)shared Calendars", async function() { + const parentColl = `/SOGo/dav/${config.subscriber_username}/Calendar/` + let results + + createdRsrc = `${parentColl}test-dav-acl/` + for (const rsrc of ['personal', 'test-dav-acl']) { + const resource = `${parentColl}${rsrc}/` + await webdav_su.makeCalendar(resource) + await utility_subscriber.setupRights(resource, config.username, {}) + } + + results = await webdav_subscriber.propfindURL(parentColl) + const hrefs = results.map(r => r.href).filter(h => { + return h == `${parentColl}` || + h.indexOf(`${parentColl}personal`) == 0 || + h.indexOf(`${parentColl}test-dav-acl`) == 0 + }) + expect(hrefs.length) + .withContext(`Profind returns at least 3 hrefs when user ${config.subscriber_username} is the owner`) + .toBeGreaterThan(2) + const [href] = hrefs + expect(href) + .withContext('Unique href must be the Calendar parent collection') + .toBe(parentColl) + }) + +}) \ No newline at end of file diff --git a/Tests/spec/DAVCalendarSuperUserAclSpec.js b/Tests/spec/DAVCalendarSuperUserAclSpec.js new file mode 100644 index 000000000..3ad6fdda6 --- /dev/null +++ b/Tests/spec/DAVCalendarSuperUserAclSpec.js @@ -0,0 +1,116 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +describe('DAVCalendarSuperUserAcl', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + const utility = new TestUtility(webdav) + + const event_template = `BEGIN:VCALENDAR +PRODID:-//Inverse//Event Generator//EN +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:0 +TRANSP:OPAQUE +UID:12345-%(class)-%(filename) +SUMMARY:%(class) event (orig. title) +DTSTART:20090805T100000Z +DTEND:20090805T140000Z +CLASS:%(class) +DESCRIPTION:%(class) description +LOCATION:location +%(organizer_line)%(attendee_line)CREATED:20090805T100000Z +DTSTAMP:20090805T100000Z +END:VEVENT +END:VCALENDAR` + + const resource = `/SOGo/dav/${config.subscriber_username}/Calendar/test-dav-superuser-acl/` + const filename = 'suevent.ics' + + const event = utility.formatTemplate(event_template, { + 'class': 'PUBLIC', + 'filename': filename + }) + + beforeAll(async function() { + await webdav_su.deleteObject(resource) + await webdav_su.makeCalendar(resource) + }) + + afterAll(async function() { + await webdav_su.deleteObject(resource) + }) + + // DAVCalendarSuperUserAclTest.testSUAccess + it("create, read, modify, delete for superuser", async function() { + let result, results + + // 1. Create + + result = await webdav_su.createCalendarObject(resource, filename, event) + expect(result.status) + .withContext('Event creation returns status code 201') + .toBe(201) + + // 2. Read - GET + + results = await webdav_su.getObject(resource, filename) + expect(results.length).toBe(1) + expect(results[0].raw.replace(/\r\n/g,'\n')).toBe(event) + + // 2. Read - PROPFIND calendar-data + + results = await webdav_su.propfindEvent(resource + filename) + expect(results.length).toBe(2) // suevent.ics + suevent.ics/master + expect(results.find(o => { + if (o.href == resource + filename) { + expect(o.props.calendarData.replace(/\r\n/g,'\n')).toBe(event) + return true + } + return false + })).toBeTruthy() + + // 2. Read - REPORT calendar-multiget + + results = await webdav_su.calendarMultiGet(resource, filename) + expect(results.length).toBe(1) + expect(results.find(o => { + if (o.href == resource + filename) { + expect(o.props.calendarData.replace(/\r\n/g,'\n')).toBe(event) + return true + } + return false + })).toBeTruthy() + + // 2. Read - webdav-sync + + results = await webdav_su.syncCollection(resource) + expect(results.length).toBe(1) + expect(results.find(o => { + expect(o.status).toBe(207) + if (o.href == resource + filename) { + expect(o.props.calendarData.replace(/\r\n/g,'\n')).toBe(event) + return true + } + return false + })).toBeTruthy() + + // 3. Modify + + const classes = ['CONFIDENTIAL', 'PRIVATE', 'PUBLIC'] + for (const c of classes) { + const event = utility.formatTemplate(event_template, { + 'class': c, + 'filename': filename + }) + const response = await webdav_su.createCalendarObject(resource, filename, event) + expect(response.status).toBe(204) + } + + // 4. Delete + const response = await webdav_su.deleteObject(resource) + expect(response.status).toBe(204) + }) + +}) \ No newline at end of file diff --git a/Tests/spec/DAVContactsCategoriesSpec.js b/Tests/spec/DAVContactsCategoriesSpec.js new file mode 100644 index 000000000..d21b2c94a --- /dev/null +++ b/Tests/spec/DAVContactsCategoriesSpec.js @@ -0,0 +1,68 @@ +import config from '../lib/config' +import { default as WebDAV, DAVInverse } from '../lib/WebDAV' + +describe('contacts categories', function() { + const webdav = new WebDAV(config.username, config.password) + + const _setCategories = async function(categories = []) { + const resource = `/SOGo/dav/${config.username}/Contacts/` + const elements = categories.map(c => { + return { 'category': c } + }) + const properties = { 'contacts-categories': elements.length ? elements : '' } + + const results = await webdav.proppatchWebdav(resource, properties, DAVInverse) + expect(results.length) + .withContext(`Set contacts categories to ${categories.join(', ')}`) + .toBe(1) + + return results[0].status + } + + const _getCategories = async function() { + const resource = `/SOGo/dav/${config.username}/Contacts/` + const properties = ['contacts-categories'] + + const results = await webdav.propfindWebdav(resource, properties, DAVInverse) + expect(results.length) + .toBe(1) + const { props: { contactsCategories: { category } = {} } = {} } = results[0] + + return category + } + + // HTTPContactCategoriesTest + + it('setting contacts categories', async function() { + let status, results + + status = await _setCategories() + expect(status) + .withContext('Removing contacts categories') + .toBe(207) + results = await _getCategories() + expect(results) + .toBeUndefined() + + status = await _setCategories(['Coucou']) + expect(status) + .withContext('Setting one contacts category') + .toBe(207) + results = await _getCategories() + expect(results) + .toBe('Coucou') + + status = await _setCategories(['Toto', 'Cuicui']) + expect(status) + .withContext('Setting two contacts category') + .toBe(207) + + results = await _getCategories() + expect(results.length) + .toBe(2) + expect(results) + .toContain('Toto') + expect(results) + .toContain('Cuicui') + }) +}) \ No newline at end of file diff --git a/Tests/spec/DAVPublicAccessSpec.js b/Tests/spec/DAVPublicAccessSpec.js new file mode 100644 index 000000000..d837dd067 --- /dev/null +++ b/Tests/spec/DAVPublicAccessSpec.js @@ -0,0 +1,39 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' + +describe('public access', function() { + const webdav_anon = new WebDAV() + + // DAVPublicAccessTest + + it("access to /SOGo/so/public", async function() { + const [{ status }] = await webdav_anon.options('/SOGo/so/public') + expect(status) + .withContext('/SOGo/so/public must not be accessible') + .toBe(404) + }) + + it("access to /SOGo/public", async function() { + const [{ status }] = await webdav_anon.options('/SOGo/public') + expect(status) + .withContext('/SOGo/public must not be accessible') + .toBe(404) + }) + + it("access to non-public resource", async function() { + const [{ status }] = await webdav_anon.options(`/SOGo/dav/${config.username}`) + expect(status) + .withContext('DAV non-public resources should request authentication') + .toBe(401) + }) + + it("access to public resource", async function() { + const [{ status }] = await webdav_anon.options('/SOGo/dav/public') + expect(status) + .withContext('DAV public resources must not request authentication') + .not.toBe(401) + expect(status) + .withContext('DAV public resources must be accessible') + .toBe(200) + }) +}) \ No newline at end of file diff --git a/Tests/spec/SogoToolSpec.js b/Tests/spec/SogoToolSpec.js new file mode 100644 index 000000000..076dc1077 --- /dev/null +++ b/Tests/spec/SogoToolSpec.js @@ -0,0 +1,35 @@ +import config from '../lib/config' +import { mkdtempSync, rmSync } from 'fs' + +const os = require('os') +const path = require('path') +const { execSync } = require('child_process') + +describe('sogo-tool tests', function() { + let tmpdir, isRoot + + beforeAll(function() { + const { uid } = os.userInfo() + isRoot = (uid == 0) + }) + + beforeEach(function() { + tmpdir = mkdtempSync(path.join(os.tmpdir(), 'sogo-')) + if (isRoot) { + execSync(`chown -R sogo:sogo ${tmpdir}`) + } + }) + + afterEach(function() { + rmSync(tmpdir, { recursive: true, force: true }) + }) + + it('backup', async function() { + const sudo = isRoot ? `sudo -u sogo ` : `` + try { + execSync(`${sudo}sogo-tool backup ${tmpdir} ${config.username} 2>&1`) + } catch (err) { + fail(err) + } + }) +}) \ No newline at end of file diff --git a/Tests/spec/WebDAVSpec.js b/Tests/spec/WebDAVSpec.js new file mode 100644 index 000000000..44516ffc5 --- /dev/null +++ b/Tests/spec/WebDAVSpec.js @@ -0,0 +1,120 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +describe('WebDAV', function() { + var webdav + var utility + + beforeEach(function() { + webdav = new WebDAV(config.username, config.password) + utility = new TestUtility(webdav) + }) + + it("property: 'principal-collection-set' on collection object", async function() { + const resource = `/SOGo/dav/${config.username}/` + const results = await webdav.principalCollectionSet(resource) + expect(results.length).toBe(1) + results.forEach(o => { + expect(o.ok).toBe(true) + expect(o.status).toBe(207) + expect(resource).toBe(o.href) + }) + }) + + it("property: 'principal-collection-set' on non-collection object", function() { + const resource = `/SOGo/dav/${config.username}/freebusy.ifb` + return webdav.principalCollectionSet(resource).then(function(results) { + expect(results.length).toBe(1) + results.forEach(o => { + expect(o.ok).toBe(true) + expect(o.status).toBe(207) + }) + }) + }) + + it("propfind: ensure various NSURL work-arounds", async function() { + const resultsNoSlash = await webdav.propfindURL(`/SOGo/dav/${config.username}`) + resultsNoSlash.forEach(o => { + // Expect no trailing slash nowhere + expect(o.href.slice(-1)).toMatch(/[^\/]$/) + }) + const resultsWithSlash = await webdav.propfindURL(`/SOGo/dav/${config.username}/`) + resultsWithSlash.forEach(o => { + // Expect a trailing slash for collections only + if (o.props.resourcetype.collection) { + expect(o.href.slice(-1)).toMatch(/\/$/) + } + else { + expect(o.href.slice(-1)).toMatch(/[^\/]$/) + } + }) + const resultsNoColl = await webdav.propfindURL(`/SOGo/dav/${config.username}/freebusy.ifb`) + resultsNoColl.forEach(o => { + // Expect no collection + expect(o.props.resourcetype.collection).toBeFalsy() + }) + }) + + // REPORT + it("principal-property-search", async function() { + const resource = `/SOGo/dav/${config.username}/Calendar` + const user = await utility.fetchUserInfo(config.username) + const results = await webdav.principalPropertySearch(resource) + expect(results.length).toBe(1) + results.forEach(o => { + expect(o.status) + .withContext(`HTTP status code when a performing a property search`) + .toBe(207) + expect(o.href).toBe(`/SOGo/dav/${config.username}/`) + expect(o.props.displayname).toBe(user.displayname) + }) + }) + + // http://tools.ietf.org/html/rfc3253.html#section-3.8 + it("expand-property", async function () { + const resource = `/SOGo/dav/${config.username}/` + const user = await utility.fetchUserInfo(config.username) + const properties = [ + { + _attributes: { + name: 'owner' + }, + property: { _attributes: { name: 'displayname' } } + }, + { + _attributes: { + name: 'principal-collection-set' + }, + property: { _attributes: { name: 'displayname' } } + } + ] + const outcomes = { + owner: { + href: resource, + displayname: user.displayname + }, + principalCollectionSet: { + href: '/SOGo/dav/', + displayname: 'SOGo' + } + } + const results = await webdav.expendProperty(resource, properties) + expect(results.length).toBe(1) + results.forEach(o => { + const { props = {} } = o + expect(o.status) + .withContext(`HTTP status code when expanding properties`) + .toBe(207) + Object.keys(outcomes).forEach(p => { + const { response: { href, propstat: { prop: { displayname }} }} = props[p] + expect(href) + .withContext(`Result of expand-property for href`) + .toBe(outcomes[p].href) + expect(displayname) + .withContext(`Result of expand-property for displayname`) + .toBe(outcomes[p].displayname) + }) + }) + }) +}) \ No newline at end of file diff --git a/Tests/spec/WebDavSyncSpec.js b/Tests/spec/WebDavSyncSpec.js new file mode 100644 index 000000000..d6cbb4bb0 --- /dev/null +++ b/Tests/spec/WebDavSyncSpec.js @@ -0,0 +1,61 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import { DAVNamespace, DAVNamespaceShorthandMap } from 'tsdav' +import convert from 'xml-js' + +describe('webdav sync', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + // const resource = `/SOGo/dav/${config.username}/Calendar/test-webdavsync/` + const resource = `/SOGo/dav/${config.username}/Calendar/personal/` + + afterEach(async function() { + // await webdav_su.deleteObject(resource) + }) + + it('webdav sync', async function() { + const nsShort = DAVNamespaceShorthandMap[DAVNamespace.DAV].toUpperCase() + let response, xml, token + + // missing tests: + // invalid tokens: negative, non-numeric, > current timestamp + // non-empty collections: token validity, status codes for added, + // modified and removed elements + + // response = await webdav.makeCalendar(resource) + // expect(response.length).toBe(1) + // expect(response[0].status) + // .withContext(`HTTP status code when creating a Calendar`) + // .toBe(201) + + // test queries: + // empty collection: + // without a token (query1) + // with a token (query2) + // (when done, non-empty collection: + // without a token (query3) + // with a token (query4)) + + response = await webdav.syncCollectionRaw(resource, null, [ 'getetag' ]) + xml = await response.text(); + ({ [`${nsShort}:multistatus`]: { [`${nsShort}:sync-token`]: { _text: token } } } = convert.xml2js(xml, {compact: true, nativeType: true})) + expect(response.status) + .withContext(`HTTP status code when performing sync-query without a token`) + .toBe(207) + expect(token) + .withContext(`Sync query returns valid token`) + .toBeGreaterThanOrEqual(0) + + // we make sure that any token is accepted when the collection is + // empty, but that the returned token differs + response = await webdav.syncCollectionRaw(resource, '1234', [ 'getetag' ]) + xml = await response.text(); + ({ [`${nsShort}:multistatus`]: { [`${nsShort}:sync-token`]: { _text: token } } } = convert.xml2js(xml, {compact: true, nativeType: true})) + expect(response.status) + .withContext(`HTTP status code when performing sync-query with a token`) + .toBe(207) + expect(token) + .withContext(`Sync query returns valid token`) + .toBeGreaterThanOrEqual(0) + }) +}) \ No newline at end of file diff --git a/Tests/spec/support/jasmine.json b/Tests/spec/support/jasmine.json new file mode 100644 index 000000000..96b797278 --- /dev/null +++ b/Tests/spec/support/jasmine.json @@ -0,0 +1,14 @@ +{ + "spec_dir": "spec", + "spec_files": [ + "**/*[sS]pec.?(m)js" + ], + "helpers": [ + "helpers/**/*.?(m)js" + ], + "requires": [ + "esm" + ], + "stopSpecOnExpectationFailure": false, + "random": true +} diff --git a/UI/MailPartViewers/UIxMailPartHTMLViewer.m b/UI/MailPartViewers/UIxMailPartHTMLViewer.m index 076c74ae4..9f5176593 100644 --- a/UI/MailPartViewers/UIxMailPartHTMLViewer.m +++ b/UI/MailPartViewers/UIxMailPartHTMLViewer.m @@ -299,7 +299,7 @@ _xmlCharsetForCharset (NSString *charset) attributes: (id ) _attributes { unsigned int count, max; - NSString *name, *value, *cid, *lowerName; + NSString *name, *value, *cid, *lowerName, *lowerValue; NSMutableString *resultPart; BOOL skipAttribute; @@ -340,7 +340,8 @@ _xmlCharsetForCharset (NSString *charset) { skipAttribute = NO; name = [[_attributes nameAtIndex: count] lowercaseString]; - if ([name hasPrefix: @"ON"]) + if ([name hasPrefix: @"on"]) + // on Events skipAttribute = YES; else if ([name isEqualToString: @"src"]) { @@ -370,13 +371,19 @@ _xmlCharsetForCharset (NSString *charset) name = [NSString stringWithFormat: @"unsafe-%@", name]; } else if ([name isEqualToString: @"href"] - || [name isEqualToString: @"action"]) + || [name isEqualToString: @"action"] + || [name isEqualToString: @"formaction"]) { value = [_attributes valueAtIndex: count]; - skipAttribute = ([value rangeOfString: @"://"].location - == NSNotFound - && ![value hasPrefix: @"mailto:"] - && ![value hasPrefix: @"#"]); + lowerValue = [[value lowercaseString] stringByReplacingString: @"\"" + withString: @""]; + skipAttribute = + ([lowerValue rangeOfString: @"://"].location == NSNotFound + && ![lowerValue hasPrefix: @"mailto:"] + && ![lowerValue hasPrefix: @"#"]) + || [lowerValue rangeOfString: @"javascript:"].location != NSNotFound; + if (!skipAttribute) + [resultPart appendString: @" rel=\"noopener\""]; } // Avoid:
else if ([name isEqualToString: @"style"]) @@ -385,39 +392,6 @@ _xmlCharsetForCharset (NSString *charset) if ([value rangeOfString: @"url" options: NSCaseInsensitiveSearch].location != NSNotFound) name = [NSString stringWithFormat: @"unsafe-%@", name]; } - else if ( - // Mouse Events - [name isEqualToString: @"onclick"] || - [name isEqualToString: @"ondblclick"] || - [name isEqualToString: @"onmousedown"] || - [name isEqualToString: @"onmousemove"] || - [name isEqualToString: @"onmouseout"] || - [name isEqualToString: @"onmouseup"] || - [name isEqualToString: @"onmouseover"] || - - // Keyboard Events - [name isEqualToString: @"onkeydown"] || - [name isEqualToString: @"onkeypress"] || - [name isEqualToString: @"onkeyup"] || - - // Frame/Object Events - [name isEqualToString: @"onabort"] || - [name isEqualToString: @"onerror"] || - [name isEqualToString: @"onload"] || - [name isEqualToString: @"onresize"] || - [name isEqualToString: @"onscroll"] || - [name isEqualToString: @"onunload"] || - - // Form Events - [name isEqualToString: @"onblur"] || - [name isEqualToString: @"onchange"] || - [name isEqualToString: @"onfocus"] || - [name isEqualToString: @"onreset"] || - [name isEqualToString: @"onselect"] || - [name isEqualToString: @"onsubmit"]) - { - skipAttribute = YES; - } else value = [_attributes valueAtIndex: count]; diff --git a/UI/MailPartViewers/UIxMailRenderingContext.m b/UI/MailPartViewers/UIxMailRenderingContext.m index ca76bc739..837ac1357 100644 --- a/UI/MailPartViewers/UIxMailRenderingContext.m +++ b/UI/MailPartViewers/UIxMailRenderingContext.m @@ -47,8 +47,7 @@ s = [[info objectForKey:@"disposition"] objectForKey: @"type"]; - shouldDisplay = (s && ([s caseInsensitiveCompare: @"ATTACHMENT"] - == NSOrderedSame)); + shouldDisplay = (s && ([s caseInsensitiveCompare: @"ATTACHMENT"] == NSOrderedSame)); if (!shouldDisplay && !textPart) shouldDisplay = ([[info objectForKey: @"bodyId"] length] ? YES : NO); @@ -216,7 +215,7 @@ static BOOL showNamedTextAttachmentsInline = NO; // TIFF files aren't well-supported and Thunderbird sometimes send PDF // files over as image/pdf ! if ([mt isEqualToString:@"image"] && - !([st isEqualToString: @"tiff"] || [st isEqualToString: @"pdf"])) + !([st isEqualToString: @"tiff"] || [st isEqualToString: @"pdf"] || [st hasSuffix: @"xml"])) { if ([self _shouldDisplayAsAttachment: _info textPart: NO]) return [self linkViewer]; diff --git a/UI/SOGoUI/SOGoAptFormatter.m b/UI/SOGoUI/SOGoAptFormatter.m index 163606d42..07c981620 100644 --- a/UI/SOGoUI/SOGoAptFormatter.m +++ b/UI/SOGoUI/SOGoAptFormatter.m @@ -65,43 +65,43 @@ /* accessors */ - (void)setTooltip { - self->formatAction = @selector(tooltipForApt::); + self->formatAction = @selector(tooltipForApt:_refDate:); } - (void)setSingleLineFullDetails { - self->formatAction = @selector(singleLineFullDetailsForApt::); + self->formatAction = @selector(singleLineFullDetailsForApt:_refDate:); } - (void)setFullDetails { - self->formatAction = @selector(fullDetailsForApt::); + self->formatAction = @selector(fullDetailsForApt:_refDate:); } - (void)setPrivateTooltip { - self->formatAction = @selector(tooltipForPrivateApt::); + self->formatAction = @selector(tooltipForPrivateApt:_refDate:); } - (void)setPrivateDetails { - self->formatAction = @selector(detailsForPrivateApt::); + self->formatAction = @selector(detailsForPrivateApt:_refDate:); } - (void)setTitleOnly { - self->formatAction = @selector(titleForApt::); + self->formatAction = @selector(titleForApt:_refDate:); } - (void)setShortTitleOnly { - self->formatAction = @selector(shortTitleForApt::); + self->formatAction = @selector(shortTitleForApt:_refDate:); } - (void)setShortMonthTitleOnly { - self->formatAction = @selector(shortMonthTitleForApt::); + self->formatAction = @selector(shortMonthTitleForApt:_refDate:); } - (void)setPrivateSuppressAll { - self->formatAction = @selector(suppressApt::); + self->formatAction = @selector(suppressApt:_refDate:); } - (void)setPrivateTitleOnly { - self->formatAction = @selector(titleOnlyForPrivateApt::); + self->formatAction = @selector(titleOnlyForPrivateApt:_refDate:); } - (void)setPrivateTitle:(NSString *)_privateTitle { @@ -218,7 +218,7 @@ - (NSString *)titleForApt:(id)_apt :(NSCalendarDate *)_refDate { NSString *title; - + title = [_apt valueForKey:@"title"]; if (![title isNotEmpty]) title = [self titlePlaceholder]; @@ -227,11 +227,11 @@ - (NSString *)shortTitleForApt:(id)_apt :(NSCalendarDate *)_refDate { NSString *title; - + title = [self titleForApt:_apt :_refDate]; if ([title length] > 50) title = [[title substringToIndex: 49] stringByAppendingString:@"..."]; - + return title; } @@ -248,14 +248,14 @@ [self appendTimeInfoForDate: startDate usingReferenceDate: nil toBuffer: title]; [title appendFormat: @" %@", [self titleForApt:_apt :_refDate]]; - + return title; } - (NSString *)singleLineFullDetailsForApt:(id)_apt :(NSCalendarDate *)_refDate { NSMutableString *aptDescr; NSString *s; - + aptDescr = [NSMutableString stringWithCapacity:60]; [self appendTimeInfoFromApt:_apt usingReferenceDate:_refDate @@ -288,7 +288,7 @@ s = [self shortTitleForApt: _apt : _refDate]; if ([s length] > 0) [aptDescr appendFormat:@"
%@", s]; - + return aptDescr; } @@ -311,7 +311,7 @@ : (NSCalendarDate *) _refDate { NSString *s; - + s = [self privateTitle]; if (!s) s = @""; @@ -348,11 +348,11 @@ { NSMutableString *aptDescr; NSString *s; - + aptDescr = [NSMutableString stringWithCapacity: 25]; [self appendTimeInfoFromApt: _apt usingReferenceDate: _refDate - toBuffer: aptDescr]; + toBuffer: aptDescr]; if ((s = [self privateTitle]) != nil) [aptDescr appendFormat:@"\n%@", s]; diff --git a/UI/WebServerResources/MailerUI.css b/UI/WebServerResources/MailerUI.css index f5ca6ac87..c67564d09 100644 --- a/UI/WebServerResources/MailerUI.css +++ b/UI/WebServerResources/MailerUI.css @@ -1,5 +1,5 @@ /* - Copyright (C) 2005-2013 Inverse inc. + Copyright (C) 2005-2022 Inverse inc. Copyright (C) 2005 SKYRIX Software AG This file is part of SOGo. @@ -425,7 +425,8 @@ DIV.mailer_mailcontent left: 0px; right: 0px; bottom: 0px; - overflow: auto; } + overflow: auto; + transform: translateX(0); } DIV.mailer_mailcontent TABLE { diff --git a/UI/WebServerResources/SOGoRootPage.js b/UI/WebServerResources/SOGoRootPage.js index 333ca809a..7425bca67 100644 --- a/UI/WebServerResources/SOGoRootPage.js +++ b/UI/WebServerResources/SOGoRootPage.js @@ -7,11 +7,6 @@ function initLogin() { date.setTime(date.getTime() - 86400000); var href = $("connectForm").action.split("/"); - var appName = href[href.length-2]; - - document.cookie = ("0xHIGHFLYxSOGo=discarded" - + "; expires=" + date.toGMTString() - + "; path=/" + appName + "/"); var about = $("about"); if (about) { @@ -118,9 +113,7 @@ function onLoginCallback(http) { if (http.status == 200) { // Make sure browser's cookies are enabled - var loginCookie = readLoginCookie(); - - if (!loginCookie) { + if (navigator && !navigator.cookieEnabled) { SetLogMessage("errorMessage", _("cookiesNotEnabled")); submitBtn.disabled = false; return; diff --git a/UI/WebServerResources/generic.js b/UI/WebServerResources/generic.js index fa1f4b3a3..a4b4ca40b 100644 --- a/UI/WebServerResources/generic.js +++ b/UI/WebServerResources/generic.js @@ -2329,17 +2329,6 @@ function readCookie(name) { return foundCookie; } -function readLoginCookie() { - var loginValues = null; - var cookie = readCookie("0xHIGHFLYxSOGo"); - if (cookie && cookie.length > 8) { - var value = decodeURIComponent(cookie.substr(8)); - loginValues = value.base64decode().split(":"); - } - - return loginValues; -} - /* logging widgets */ function SetLogMessage(containerId, message, msgType) { var container = $(containerId); diff --git a/Version b/Version index c20f5557a..27469aff1 100644 --- a/Version +++ b/Version @@ -1,3 +1,3 @@ MAJOR_VERSION=2 MINOR_VERSION=4 -SUBMINOR_VERSION=1 +SUBMINOR_VERSION=2 diff --git a/packaging/debian/control b/packaging/debian/control index 626225f1a..bc1a5d28a 100644 --- a/packaging/debian/control +++ b/packaging/debian/control @@ -8,9 +8,8 @@ Standards-Version: 3.9.1 Package: sogo Section: web Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends}, tmpreaper, sope4.9-libxmlsaxdriver, sope4.9-db-connector, gnustep-make, libcurl3 | libcurl4, zip, liblasso3 (>= 2.3.5) -Recommends: memcached -Suggests: nginx +Depends: ${shlibs:Depends}, ${misc:Depends}, sope4.9-libxmlsaxdriver, sope4.9-db-connector, gnustep-make, libcurl3 | libcurl4, zip, liblasso3 (>= 2.3.5) +Recommends: memcached, apache2 | nginx | httpd Description: a modern and scalable groupware SOGo is a groupware server built around OpenGroupware.org (OGo) and the SOPE application server with focus on scalability. diff --git a/packaging/debian/sogo.cron.daily b/packaging/debian/sogo.cron.daily index d28a8faa8..989191b2f 100644 --- a/packaging/debian/sogo.cron.daily +++ b/packaging/debian/sogo.cron.daily @@ -1,6 +1,7 @@ #!/bin/sh +# SOGOSPOOL must match the value of the configuration parameter SOGoMailSpoolPath SOGOSPOOL=/var/spool/sogo -/usr/sbin/tmpreaper 24 "$SOGOSPOOL" +find "$SOGOSPOOL" -type f -user sogo -atime +23 -delete > /dev/null find "$SOGOSPOOL" -mindepth 1 -type d -user sogo -empty -delete > /dev/null diff --git a/packaging/rhel/sogo.spec b/packaging/rhel/sogo.spec index 0ae20352e..263dddc52 100644 --- a/packaging/rhel/sogo.spec +++ b/packaging/rhel/sogo.spec @@ -16,7 +16,7 @@ Group: Productivity/Groupware Source: SOGo-%{sogo_version}.tar.gz Prefix: /usr AutoReqProv: off -Requires: gnustep-base >= 1.23, sope%{sope_major_version}%{sope_minor_version}-core, httpd, sope%{sope_major_version}%{sope_minor_version}-core, sope%{sope_major_version}%{sope_minor_version}-appserver, sope%{sope_major_version}%{sope_minor_version}-ldap, sope%{sope_major_version}%{sope_minor_version}-cards >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-gdl1-contentstore >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-sbjson, lasso, libmemcached, memcached, libcurl, tmpwatch, zip +Requires: gnustep-base >= 1.23, sope%{sope_major_version}%{sope_minor_version}-core, httpd, sope%{sope_major_version}%{sope_minor_version}-core, sope%{sope_major_version}%{sope_minor_version}-appserver, sope%{sope_major_version}%{sope_minor_version}-ldap, sope%{sope_major_version}%{sope_minor_version}-cards >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-gdl1-contentstore >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-sbjson, lasso, libmemcached, memcached, libcurl, zip BuildRoot: %{_tmppath}/%{name}-%{version}-%{release} BuildRequires: gcc-objc gnustep-base gnustep-make sope%{sope_major_version}%{sope_minor_version}-appserver-devel sope%{sope_major_version}%{sope_minor_version}-core-devel sope%{sope_major_version}%{sope_minor_version}-ldap-devel sope%{sope_major_version}%{sope_minor_version}-mime-devel sope%{sope_major_version}%{sope_minor_version}-xml-devel sope%{sope_major_version}%{sope_minor_version}-gdl1-devel sope%{sope_major_version}%{sope_minor_version}-sbjson-devel lasso-devel libmemcached-devel sed libcurl-devel