##
# Copyright (c) 2011-2017 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

"""
Tests for calendarserver.tools.purge
"""

import os

from calendarserver.tools.purge import (
    PurgeOldEventsService, PurgeAttachmentsService, PurgePrincipalService, PrincipalPurgeHomeWork
)
from pycalendar.datetime import DateTime
from twext.enterprise.dal.syntax import Update, Delete
from twext.enterprise.util import parseSQLTimestamp
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
from twistedcaldav.config import config
from twistedcaldav.test.util import StoreTestCase
from twistedcaldav.vcard import Component as VCardComponent
from txdav.common.datastore.sql_tables import schema
from txdav.common.datastore.test.util import populateCalendarsFrom
from txweb2.http_headers import MimeType


now = DateTime.getToday().getYear()

OLD_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYMONTH=10;BYDAY=-1SU
DTSTART:19621028T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYMONTH=4;BYDAY=1SU
DTSTART:19870405T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
DTSTART:20070311T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
DTSTART:20071104T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:685BC3A1-195A-49B3-926D-388DDACA78A6
DTEND;TZID=US/Pacific:%(year)s0307T151500
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART;TZID=US/Pacific:%(year)s0307T111500
DTSTAMP:20100303T181220Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": now - 5}

ATTACHMENT_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYMONTH=10;BYDAY=-1SU
DTSTART:19621028T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYMONTH=4;BYDAY=1SU
DTSTART:19870405T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
DTSTART:20070311T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
DTSTART:20071104T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:57A5D1F6-9A57-4F74-9520-25C617F54B88-%(uid)s
TRANSP:OPAQUE
SUMMARY:Ancient event with attachment
DTSTART;TZID=US/Pacific:%(year)s0308T111500
DTEND;TZID=US/Pacific:%(year)s0308T151500
DTSTAMP:20100303T181220Z
X-APPLE-DROPBOX:/calendars/__uids__/user01/dropbox/%(dropboxid)s.dropbox
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

MATTACHMENT_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYMONTH=10;BYDAY=-1SU
DTSTART:19621028T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYMONTH=4;BYDAY=1SU
DTSTART:19870405T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
DTSTART:20070311T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
DTSTART:20071104T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:57A5D1F6-9A57-4F74-9520-25C617F54B88-%(uid)s
TRANSP:OPAQUE
SUMMARY:Ancient event with attachment
DTSTART;TZID=US/Pacific:%(year)s0308T111500
DTEND;TZID=US/Pacific:%(year)s0308T151500
DTSTAMP:20100303T181220Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

ENDLESS_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYMONTH=10;BYDAY=-1SU
DTSTART:19621028T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYMONTH=4;BYDAY=1SU
DTSTART:19870405T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
DTSTART:20070311T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
DTSTART:20071104T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20100303T194654Z
UID:9FDE0E4C-1495-4CAF-863B-F7F0FB15FE8C
DTEND;TZID=US/Pacific:%(year)s0308T151500
RRULE:FREQ=YEARLY;INTERVAL=1
TRANSP:OPAQUE
SUMMARY:Ancient Repeating Endless
DTSTART;TZID=US/Pacific:%(year)s0308T111500
DTSTAMP:20100303T194710Z
SEQUENCE:4
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": now - 5}

REPEATING_AWHILE_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYMONTH=10;BYDAY=-1SU
DTSTART:19621028T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYMONTH=4;BYDAY=1SU
DTSTART:19870405T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
DTSTART:20070311T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
DTSTART:20071104T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20100303T194716Z
UID:76236B32-2BC4-4D78-956B-8D42D4086200
DTEND;TZID=US/Pacific:%(year)s0309T151500
RRULE:FREQ=YEARLY;INTERVAL=1;COUNT=3
TRANSP:OPAQUE
SUMMARY:Ancient Repeat Awhile
DTSTART;TZID=US/Pacific:%(year)s0309T111500
DTSTAMP:20100303T194747Z
SEQUENCE:6
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": now - 5}

STRADDLING_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
DTSTART:20070311T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
DTSTART:20071104T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20100303T213643Z
UID:1C219DAD-D374-4822-8C98-ADBA85E253AB
DTEND;TZID=US/Pacific:%(year)s0508T121500
RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=%(until)s0509T065959Z
TRANSP:OPAQUE
SUMMARY:Straddling cut-off
DTSTART;TZID=US/Pacific:%(year)s0508T111500
DTSTAMP:20100303T213704Z
SEQUENCE:5
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": now - 2, "until": now + 1}

RECENT_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
DTSTART:20070311T020000
TZNAME:PDT
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
DTSTART:20071104T020000
TZNAME:PST
TZOFFSETTO:-0800
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20100303T195159Z
UID:F2F14D94-B944-43D9-8F6F-97F95B2764CA
DTEND;TZID=US/Pacific:%(year)s0304T141500
TRANSP:OPAQUE
SUMMARY:Recent
DTSTART;TZID=US/Pacific:%(year)s0304T120000
DTSTAMP:20100303T195203Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": now}


VCARD_1 = """BEGIN:VCARD
VERSION:3.0
N:User;Test
FN:Test User
EMAIL;type=INTERNET;PREF:testuser@example.com
UID:12345-67890-1.1
END:VCARD
""".replace("\n", "\r\n")


class PurgeOldEventsTests(StoreTestCase):
    """
    Tests for deleting events older than a given date
    """

    metadata = {
        "accessMode": "PUBLIC",
        "isScheduleObject": True,
        "scheduleTag": "abc",
        "scheduleEtags": (),
        "hasPrivateComment": False,
    }

    requirements = {
        "home1": {
            "calendar1": {
                "old.ics": (OLD_ICS, metadata,),
                "endless.ics": (ENDLESS_ICS, metadata,),
                "oldattachment1.ics": (ATTACHMENT_ICS % {"year": now - 5, "uid": "1.1", "dropboxid": "1.1"}, metadata,),
                "oldattachment2.ics": (ATTACHMENT_ICS % {"year": now - 5, "uid": "1.2", "dropboxid": "1.2"}, metadata,),
                "currentattachment3.ics": (ATTACHMENT_ICS % {"year": now + 1, "uid": "1.3", "dropboxid": "1.3"}, metadata,),
                "oldmattachment1.ics": (MATTACHMENT_ICS % {"year": now - 5, "uid": "1.1m"}, metadata,),
                "oldmattachment2.ics": (MATTACHMENT_ICS % {"year": now - 5, "uid": "1.2m"}, metadata,),
                "currentmattachment3.ics": (MATTACHMENT_ICS % {"year": now + 1, "uid": "1.3m"}, metadata,),
            },
            "inbox": {},
        },
        "home2": {
            "calendar2": {
                "straddling.ics": (STRADDLING_ICS, metadata,),
                "recent.ics": (RECENT_ICS, metadata,),
                "oldattachment1.ics": (ATTACHMENT_ICS % {"year": now - 5, "uid": "2.1", "dropboxid": "2.1"}, metadata,),
                "currentattachment2.ics": (ATTACHMENT_ICS % {"year": now + 1, "uid": "2.2", "dropboxid": "2.1"}, metadata,),
                "oldattachment3.ics": (ATTACHMENT_ICS % {"year": now - 5, "uid": "2.3", "dropboxid": "2.2"}, metadata,),
                "oldattachment4.ics": (ATTACHMENT_ICS % {"year": now - 5, "uid": "2.4", "dropboxid": "2.2"}, metadata,),
                "oldmattachment1.ics": (MATTACHMENT_ICS % {"year": now - 5, "uid": "2.1"}, metadata,),
                "currentmattachment2.ics": (MATTACHMENT_ICS % {"year": now + 1, "uid": "2.2"}, metadata,),
                "oldmattachment3.ics": (MATTACHMENT_ICS % {"year": now - 5, "uid": "2.3"}, metadata,),
                "oldmattachment4.ics": (MATTACHMENT_ICS % {"year": now - 5, "uid": "2.4"}, metadata,),
            },
            "calendar3": {
                "repeating_awhile.ics": (REPEATING_AWHILE_ICS, metadata,),
            },
            "inbox": {},
        }
    }

    def configure(self):
        super(PurgeOldEventsTests, self).configure()

        # Turn off delayed indexing option so we can have some useful tests
        self.patch(config, "FreeBusyIndexDelayedExpand", False)

        # Tweak queue timing to speed things up
        self.patch(config.Scheduling.Options.WorkQueues, "Enabled", False)
        self.patch(config.AutomaticPurging, "PollingIntervalSeconds", -1)
        self.patch(config.AutomaticPurging, "CheckStaggerSeconds", 1)
        self.patch(config.AutomaticPurging, "PurgeIntervalSeconds", 3)
        self.patch(config.AutomaticPurging, "HomePurgeDelaySeconds", 1)

        # self.patch(config.DirectoryService.params, "xmlFile",
        #     os.path.join(
        #         os.path.dirname(__file__), "purge", "accounts.xml"
        #     )
        # )
        # self.patch(config.ResourceService.params, "xmlFile",
        #     os.path.join(
        #         os.path.dirname(__file__), "purge", "resources.xml"
        #     )
        # )

    @inlineCallbacks
    def populate(self):
        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
        self.notifierFactory.reset()

        txn = self._sqlCalendarStore.newTransaction()
        Delete(
            From=schema.ATTACHMENT,
            Where=None
        ).on(txn)

        (yield txn.commit())

    @inlineCallbacks
    def test_eventsOlderThan(self):
        cutoff = DateTime(now, 4, 1, 0, 0, 0)
        txn = self._sqlCalendarStore.newTransaction()

        # Query for all old events
        results = (yield txn.eventsOlderThan(cutoff))
        self.assertEquals(
            sorted(results),
            sorted([
                ['home1', 'calendar1', 'old.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home1', 'calendar1', 'oldattachment1.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home1', 'calendar1', 'oldattachment2.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home1', 'calendar1', 'oldmattachment1.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home1', 'calendar1', 'oldmattachment2.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home2', 'calendar3', 'repeating_awhile.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home2', 'calendar2', 'recent.ics', parseSQLTimestamp('%s-03-04 22:15:00' % (now,))],
                ['home2', 'calendar2', 'oldattachment1.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home2', 'calendar2', 'oldattachment3.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home2', 'calendar2', 'oldattachment4.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home2', 'calendar2', 'oldmattachment1.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home2', 'calendar2', 'oldmattachment3.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
                ['home2', 'calendar2', 'oldmattachment4.ics', parseSQLTimestamp('1901-01-01 01:00:00')],
            ])
        )

        # Query for oldest event - actually with limited time caching, the oldest event
        # cannot be precisely known, all we get back is the first one in the sorted list
        # where each has the 1901 "dummy" time stamp to indicate a partial cache
        results = (yield txn.eventsOlderThan(cutoff, batchSize=1))
        self.assertEquals(len(results), 1)

    @inlineCallbacks
    def test_removeOldEvents(self):
        cutoff = DateTime(now, 4, 1, 0, 0, 0)
        txn = self._sqlCalendarStore.newTransaction()

        # Remove oldest event - except we don't know what that is because of the dummy timestamps
        # used with a partial index. So all we can check is that one event was removed.
        count = (yield txn.removeOldEvents(cutoff, batchSize=1))
        self.assertEquals(count, 1)
        results = (yield txn.eventsOlderThan(cutoff))
        self.assertEquals(len(results), 12)

        # Remove remaining oldest events
        count = (yield txn.removeOldEvents(cutoff))
        self.assertEquals(count, 12)
        results = (yield txn.eventsOlderThan(cutoff))
        self.assertEquals(list(results), [])

        # Remove oldest events (none left)
        count = (yield txn.removeOldEvents(cutoff))
        self.assertEquals(count, 0)

    @inlineCallbacks
    def _addAttachment(self, home, calendar, event, name):

        self._sqlCalendarStore._dropbox_ok = True
        txn = self._sqlCalendarStore.newTransaction()

        # Create an event with an attachment
        home = (yield txn.calendarHomeWithUID(home))
        calendar = (yield home.calendarWithName(calendar))
        event = (yield calendar.calendarObjectWithName(event))
        attachment = (yield event.createAttachmentWithName(name))
        t = attachment.store(MimeType("text", "x-fixture"))
        t.write("%s/%s/%s/%s" % (home, calendar, event, name,))
        t.write(" attachment")
        (yield t.loseConnection())

        (yield txn.commit())
        self._sqlCalendarStore._dropbox_ok = False

        returnValue(attachment)

    @inlineCallbacks
    def _orphanAttachment(self, home, calendar, event):

        txn = self._sqlCalendarStore.newTransaction()

        # Reset dropbox id in calendar_object
        home = (yield txn.calendarHomeWithUID(home))
        calendar = (yield home.calendarWithName(calendar))
        event = (yield calendar.calendarObjectWithName(event))
        co = schema.CALENDAR_OBJECT
        Update(
            {co.DROPBOX_ID: None, },
            Where=co.RESOURCE_ID == event._resourceID,
        ).on(txn)

        (yield txn.commit())

    @inlineCallbacks
    def _addManagedAttachment(self, home, calendar, event, name):

        txn = self._sqlCalendarStore.newTransaction()

        # Create an event with an attachment
        home = (yield txn.calendarHomeWithUID(home))
        calendar = (yield home.calendarWithName(calendar))
        event = (yield calendar.calendarObjectWithName(event))
        attachment = (yield event.createManagedAttachment())
        t = attachment.store(MimeType("text", "x-fixture"), name)
        t.write("%s/%s/%s/%s" % (home, calendar, event, name,))
        t.write(" managed attachment")
        (yield t.loseConnection())

        (yield txn.commit())

        returnValue(attachment)

    @inlineCallbacks
    def test_removeOrphanedAttachments(self):

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota1 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertEqual(quota1, 0)

        attachment = (yield self._addAttachment("home1", "calendar1", "oldattachment1.ics", "att1"))
        attachmentPath = attachment._path.path
        self.assertTrue(os.path.exists(attachmentPath))

        mattachment1 = (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment1.ics", "matt1"))
        mattachment2 = (yield self._addManagedAttachment("home1", "calendar1", "currentmattachment3.ics", "matt3"))

        mattachmentPath1 = mattachment1._path.path
        self.assertTrue(os.path.exists(mattachmentPath1))
        mattachmentPath2 = mattachment2._path.path
        self.assertTrue(os.path.exists(mattachmentPath2))

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota2 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota2 > quota1)

        orphans = (yield self.transactionUnderTest().orphanedAttachments())
        self.assertEquals(len(orphans), 0)

        count = (yield self.transactionUnderTest().removeOrphanedAttachments(batchSize=100))
        self.assertEquals(count, 0)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertNotEqual(quota, 0)

        # Files still exist
        self.assertTrue(os.path.exists(attachmentPath))
        self.assertTrue(os.path.exists(mattachmentPath1))
        self.assertTrue(os.path.exists(mattachmentPath2))

        # Delete all old events (including the event containing the attachment)
        cutoff = DateTime(now, 4, 1, 0, 0, 0)
        count = (yield self.transactionUnderTest().removeOldEvents(cutoff))

        # See which events have gone and which exist
        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        calendar = (yield home.calendarWithName("calendar1"))
        self.assertNotEqual((yield calendar.calendarObjectWithName("endless.ics")), None)
        self.assertNotEqual((yield calendar.calendarObjectWithName("currentmattachment3.ics")), None)
        self.assertEqual((yield calendar.calendarObjectWithName("old.ics")), None)
        self.assertEqual((yield calendar.calendarObjectWithName("oldattachment1.ics")), None)
        self.assertEqual((yield calendar.calendarObjectWithName("oldmattachment1.ics")), None)
        (yield self.commit())

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota3 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota3 < quota2)
        self.assertNotEqual(quota3, 0)

        # Just look for orphaned attachments - none left
        orphans = (yield self.transactionUnderTest().orphanedAttachments())
        self.assertEquals(len(orphans), 0)

        # Files
        self.assertFalse(os.path.exists(attachmentPath))
        self.assertFalse(os.path.exists(mattachmentPath1))
        self.assertTrue(os.path.exists(mattachmentPath2))

    @inlineCallbacks
    def test_purgeOldEvents(self):

        # Dry run
        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            None,
            DateTime(now, 4, 1, 0, 0, 0),
            2,
            dryrun=True,
            debug=True
        ))
        self.assertEquals(total, 13)

        # Actually remove
        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            None,
            DateTime(now, 4, 1, 0, 0, 0),
            2,
            debug=True
        ))
        self.assertEquals(total, 13)

        # There should be no more left
        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            None,
            DateTime(now, 4, 1, 0, 0, 0),
            2,
            debug=True
        ))
        self.assertEquals(total, 0)

    @inlineCallbacks
    def test_purgeOldEvents_home_filtering(self):

        # Dry run
        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            "ho",
            DateTime(now, 4, 1, 0, 0, 0),
            2,
            dryrun=True,
            debug=True
        ))
        self.assertEquals(total, 13)

        # Dry run
        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            "home",
            DateTime(now, 4, 1, 0, 0, 0),
            2,
            dryrun=True,
            debug=True
        ))
        self.assertEquals(total, 13)

        # Dry run
        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            "home1",
            DateTime(now, 4, 1, 0, 0, 0),
            2,
            dryrun=True,
            debug=True
        ))
        self.assertEquals(total, 5)

        # Dry run
        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            "home2",
            DateTime(now, 4, 1, 0, 0, 0),
            2,
            dryrun=True,
            debug=True
        ))
        self.assertEquals(total, 8)

    @inlineCallbacks
    def test_purgeOldEvents_old_cutoff(self):

        # Dry run
        cutoff = DateTime.getToday()
        cutoff.setDateOnly(False)
        cutoff.offsetDay(-400)

        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            "ho",
            cutoff,
            2,
            dryrun=True,
            debug=True
        ))
        self.assertEquals(total, 12)

        # Actually remove
        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            None,
            cutoff,
            2,
            debug=True
        ))
        self.assertEquals(total, 12)

        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            "ho",
            cutoff,
            2,
            dryrun=True,
            debug=True
        ))
        self.assertEquals(total, 0)

    @inlineCallbacks
    def test_purgeUID(self):
        txn = self._sqlCalendarStore.newTransaction()

        # Create an addressbook and one CardDAV resource
        abHome = (yield txn.addressbookHomeWithUID("home1", create=True))
        abColl = (yield abHome.addressbookWithName("addressbook"))
        (yield abColl.createAddressBookObjectWithName(
            "card1",
            VCardComponent.fromString(VCARD_1)))
        self.assertEquals(len((yield abColl.addressbookObjects())), 1)

        # Verify there are 8 events in calendar1
        calHome = (yield txn.calendarHomeWithUID("home1"))
        calColl = (yield calHome.calendarWithName("calendar1"))
        self.assertEquals(len((yield calColl.calendarObjects())), 8)

        # Make the newly created objects available to the purgeUID transaction
        (yield txn.commit())

        # Purge home1 completely
        total = yield PurgePrincipalService.purgeUIDs(
            self._sqlCalendarStore, self.directory,
            ("home1",), verbose=False, proxies=False)

        # Wait for queue to process
        while(True):
            txn = self.transactionUnderTest()
            work = yield PrincipalPurgeHomeWork.all(txn)
            yield self.commit()
            if len(work) == 0:
                break
            d = Deferred()
            reactor.callLater(1, lambda: d.callback(None))
            yield d

        # 9 items deleted: 8 events and 1 vcard
        self.assertEquals(total, 9)

        # Homes have been deleted as well
        txn = self._sqlCalendarStore.newTransaction()
        abHome = (yield txn.addressbookHomeWithUID("home1"))
        self.assertEquals(abHome, None)
        calHome = (yield txn.calendarHomeWithUID("home1"))
        self.assertEquals(calHome, None)

    @inlineCallbacks
    def test_purgeAttachmentsWithoutCutoffWithPurgeOld(self):
        """
        L{PurgeAttachmentsService.purgeAttachments} purges only orphaned attachments, not current ones.
        """

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota1 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertEqual(quota1, 0)

        (yield self._addAttachment("home1", "calendar1", "oldattachment1.ics", "att1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.2"))
        (yield self._addAttachment("home1", "calendar1", "currentattachment3.ics", "att3"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment1.ics", "att4"))
        (yield self._addAttachment("home2", "calendar2", "currentattachment2.ics", "att5"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment3.ics", "att6"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment4.ics", "att7"))
        (yield self._orphanAttachment("home1", "calendar1", "oldattachment1.ics"))

        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment1.ics", "matt1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.2"))
        (yield self._addManagedAttachment("home1", "calendar1", "currentmattachment3.ics", "matt3"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment1.ics", "matt4"))
        (yield self._addManagedAttachment("home2", "calendar2", "currentmattachment2.ics", "matt5"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment3.ics", "matt6"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment4.ics", "matt7"))

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota2 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota2 > quota1)

        # Remove old events first
        total = (yield PurgeOldEventsService.purgeOldEvents(
            self._sqlCalendarStore,
            None,
            DateTime(now, 4, 1, 0, 0, 0),
            2,
            debug=False
        ))
        self.assertEquals(total, 13)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota3 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota3 < quota2)

        # Dry run
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, None, 0, 2, dryrun=True, verbose=False))
        self.assertEquals(total, 1)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota4 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota4 == quota3)

        # Actually remove
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, None, 0, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 1)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota5 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota5 < quota4)

        # There should be no more left
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, None, 0, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 0)

    @inlineCallbacks
    def test_purgeAttachmentsWithoutCutoff(self):
        """
        L{PurgeAttachmentsService.purgeAttachments} purges only orphaned attachments, not current ones.
        """

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota1 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertEqual(quota1, 0)

        (yield self._addAttachment("home1", "calendar1", "oldattachment1.ics", "att1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.2"))
        (yield self._addAttachment("home1", "calendar1", "currentattachment3.ics", "att3"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment1.ics", "att4"))
        (yield self._addAttachment("home2", "calendar2", "currentattachment2.ics", "att5"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment3.ics", "att6"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment4.ics", "att7"))
        (yield self._orphanAttachment("home1", "calendar1", "oldattachment1.ics"))
        (yield self._orphanAttachment("home2", "calendar2", "oldattachment1.ics"))
        (yield self._orphanAttachment("home2", "calendar2", "currentattachment2.ics"))

        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment1.ics", "matt1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.2"))
        (yield self._addManagedAttachment("home1", "calendar1", "currentmattachment3.ics", "matt3"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment1.ics", "matt4"))
        (yield self._addManagedAttachment("home2", "calendar2", "currentmattachment2.ics", "matt5"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment3.ics", "matt6"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment4.ics", "matt7"))

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota2 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota2 > quota1)

        # Dry run
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, None, 0, 2, dryrun=True, verbose=False))
        self.assertEquals(total, 3)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota3 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota3 == quota2)

        # Actually remove
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, None, 0, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 3)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota4 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota4 < quota3)

        # There should be no more left
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, None, 0, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 0)

    @inlineCallbacks
    def test_purgeAttachmentsWithoutCutoffWithMatchingUUID(self):
        """
        L{PurgeAttachmentsService.purgeAttachments} purges only orphaned attachments, not current ones.
        """

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota1 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertEqual(quota1, 0)

        (yield self._addAttachment("home1", "calendar1", "oldattachment1.ics", "att1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.2"))
        (yield self._addAttachment("home1", "calendar1", "currentattachment3.ics", "att3"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment1.ics", "att4"))
        (yield self._addAttachment("home2", "calendar2", "currentattachment2.ics", "att5"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment3.ics", "att6"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment4.ics", "att7"))
        (yield self._orphanAttachment("home1", "calendar1", "oldattachment1.ics"))
        (yield self._orphanAttachment("home2", "calendar2", "oldattachment1.ics"))
        (yield self._orphanAttachment("home2", "calendar2", "currentattachment2.ics"))

        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment1.ics", "matt1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.2"))
        (yield self._addManagedAttachment("home1", "calendar1", "currentmattachment3.ics", "matt3"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment1.ics", "matt4"))
        (yield self._addManagedAttachment("home2", "calendar2", "currentmattachment2.ics", "matt5"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment3.ics", "matt6"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment4.ics", "matt7"))

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota2 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota2 > quota1)

        # Dry run
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home1", 0, 2, dryrun=True, verbose=False))
        self.assertEquals(total, 1)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota3 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota3 == quota2)

        # Actually remove
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home1", 0, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 1)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota4 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota4 < quota3)

        # There should be no more left
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home1", 0, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 0)

    @inlineCallbacks
    def test_purgeAttachmentsWithoutCutoffWithoutMatchingUUID(self):
        """
        L{PurgeAttachmentsService.purgeAttachments} purges only orphaned attachments, not current ones.
        """

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota1 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertEqual(quota1, 0)

        (yield self._addAttachment("home1", "calendar1", "oldattachment1.ics", "att1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.2"))
        (yield self._addAttachment("home1", "calendar1", "currentattachment3.ics", "att3"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment1.ics", "att4"))
        (yield self._addAttachment("home2", "calendar2", "currentattachment2.ics", "att5"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment3.ics", "att6"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment4.ics", "att7"))
        (yield self._orphanAttachment("home1", "calendar1", "oldattachment1.ics"))

        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment1.ics", "matt1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.2"))
        (yield self._addManagedAttachment("home1", "calendar1", "currentmattachment3.ics", "matt3"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment1.ics", "matt4"))
        (yield self._addManagedAttachment("home2", "calendar2", "currentmattachment2.ics", "matt5"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment3.ics", "matt6"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment4.ics", "matt7"))

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota2 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota2 > quota1)

        # Dry run
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home2", 0, 2, dryrun=True, verbose=False))
        self.assertEquals(total, 0)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota3 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota3 == quota2)

        # Actually remove
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home2", 0, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 0)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota4 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota4 == quota3)

        # There should be no more left
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home2", 0, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 0)

    @inlineCallbacks
    def test_purgeAttachmentsWithCutoffOld(self):
        """
        L{PurgeAttachmentsService.purgeAttachments} purges only orphaned attachments, not current ones.
        """

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota1 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertEqual(quota1, 0)

        (yield self._addAttachment("home1", "calendar1", "oldattachment1.ics", "att1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.2"))
        (yield self._addAttachment("home1", "calendar1", "currentattachment3.ics", "att3"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment1.ics", "att4"))
        (yield self._addAttachment("home2", "calendar2", "currentattachment2.ics", "att5"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment3.ics", "att6"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment4.ics", "att7"))
        (yield self._orphanAttachment("home1", "calendar1", "oldattachment1.ics"))
        (yield self._orphanAttachment("home2", "calendar2", "oldattachment1.ics"))
        (yield self._orphanAttachment("home2", "calendar2", "currentattachment2.ics"))

        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment1.ics", "matt1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.2"))
        (yield self._addManagedAttachment("home1", "calendar1", "currentmattachment3.ics", "matt3"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment1.ics", "matt4"))
        (yield self._addManagedAttachment("home2", "calendar2", "currentmattachment2.ics", "matt5"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment3.ics", "matt6"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment4.ics", "matt7"))

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota2 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota2 > quota1)

        # Dry run
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, None, 14, 2, dryrun=True, verbose=False))
        self.assertEquals(total, 13)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota3 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota3 == quota2)

        # Actually remove
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, None, 14, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 13)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota4 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota4 < quota3)

        # There should be no more left
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, None, 14, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 0)

    @inlineCallbacks
    def test_purgeAttachmentsWithCutoffOldWithMatchingUUID(self):
        """
        L{PurgeAttachmentsService.purgeAttachments} purges only orphaned attachments, not current ones.
        """

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota1 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertEqual(quota1, 0)

        (yield self._addAttachment("home1", "calendar1", "oldattachment1.ics", "att1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.2"))
        (yield self._addAttachment("home1", "calendar1", "currentattachment3.ics", "att3"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment1.ics", "att4"))
        (yield self._addAttachment("home2", "calendar2", "currentattachment2.ics", "att5"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment3.ics", "att6"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment4.ics", "att7"))
        (yield self._orphanAttachment("home1", "calendar1", "oldattachment1.ics"))
        (yield self._orphanAttachment("home2", "calendar2", "oldattachment1.ics"))
        (yield self._orphanAttachment("home2", "calendar2", "currentattachment2.ics"))

        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment1.ics", "matt1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.2"))
        (yield self._addManagedAttachment("home1", "calendar1", "currentmattachment3.ics", "matt3"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment1.ics", "matt4"))
        (yield self._addManagedAttachment("home2", "calendar2", "currentmattachment2.ics", "matt5"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment3.ics", "matt6"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment4.ics", "matt7"))

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota2 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota2 > quota1)

        # Dry run
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home1", 14, 2, dryrun=True, verbose=False))
        self.assertEquals(total, 6)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota3 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota3 == quota2)

        # Actually remove
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home1", 14, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 6)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota4 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota4 < quota3)

        # There should be no more left
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home1", 14, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 0)

    @inlineCallbacks
    def test_purgeAttachmentsWithCutoffOldWithoutMatchingUUID(self):
        """
        L{PurgeAttachmentsService.purgeAttachments} purges only orphaned attachments, not current ones.
        """

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota1 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertEqual(quota1, 0)

        (yield self._addAttachment("home1", "calendar1", "oldattachment1.ics", "att1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.1"))
        (yield self._addAttachment("home1", "calendar1", "oldattachment2.ics", "att2.2"))
        (yield self._addAttachment("home1", "calendar1", "currentattachment3.ics", "att3"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment1.ics", "att4"))
        (yield self._addAttachment("home2", "calendar2", "currentattachment2.ics", "att5"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment3.ics", "att6"))
        (yield self._addAttachment("home2", "calendar2", "oldattachment4.ics", "att7"))
        (yield self._orphanAttachment("home1", "calendar1", "oldattachment1.ics"))
        (yield self._orphanAttachment("home2", "calendar2", "oldattachment1.ics"))
        (yield self._orphanAttachment("home2", "calendar2", "currentattachment2.ics"))

        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment1.ics", "matt1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.1"))
        (yield self._addManagedAttachment("home1", "calendar1", "oldmattachment2.ics", "matt2.2"))
        (yield self._addManagedAttachment("home1", "calendar1", "currentmattachment3.ics", "matt3"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment1.ics", "matt4"))
        (yield self._addManagedAttachment("home2", "calendar2", "currentmattachment2.ics", "matt5"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment3.ics", "matt6"))
        (yield self._addManagedAttachment("home2", "calendar2", "oldmattachment4.ics", "matt7"))

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota2 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota2 > quota1)

        # Dry run
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home2", 14, 2, dryrun=True, verbose=False))
        self.assertEquals(total, 7)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota3 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota3 == quota2)

        # Actually remove
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home2", 14, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 7)

        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
        quota4 = (yield home.quotaUsedBytes())
        (yield self.commit())
        self.assertTrue(quota4 == quota3)

        # There should be no more left
        total = (yield PurgeAttachmentsService.purgeAttachments(self._sqlCalendarStore, "home2", 14, 2, dryrun=False, verbose=False))
        self.assertEquals(total, 0)
