aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/tests/unit')
-rw-r--r--apps/dav/tests/unit/AppInfo/ApplicationTest.php34
-rw-r--r--apps/dav/tests/unit/AppInfo/PluginManagerTest.php126
-rw-r--r--apps/dav/tests/unit/Avatars/AvatarHomeTest.php99
-rw-r--r--apps/dav/tests/unit/Avatars/AvatarNodeTest.php33
-rw-r--r--apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php81
-rw-r--r--apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php170
-rw-r--r--apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php72
-rw-r--r--apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php113
-rw-r--r--apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php148
-rw-r--r--apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php82
-rw-r--r--apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php96
-rw-r--r--apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php79
-rw-r--r--apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php47
-rw-r--r--apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php220
-rw-r--r--apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php263
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/BackendTest.php349
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php67
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php78
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php67
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php116
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php190
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php83
-rw-r--r--apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php120
-rw-r--r--apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php170
-rw-r--r--apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php221
-rw-r--r--apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php80
-rw-r--r--apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php76
-rw-r--r--apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php72
-rw-r--r--apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php296
-rw-r--r--apps/dav/tests/unit/CalDAV/CalDavBackendTest.php1885
-rw-r--r--apps/dav/tests/unit/CalDAV/CalendarHomeTest.php347
-rw-r--r--apps/dav/tests/unit/CalDAV/CalendarImplTest.php308
-rw-r--r--apps/dav/tests/unit/CalDAV/CalendarManagerTest.php64
-rw-r--r--apps/dav/tests/unit/CalDAV/CalendarTest.php608
-rw-r--r--apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php171
-rw-r--r--apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php188
-rw-r--r--apps/dav/tests/unit/CalDAV/EventReaderTest.php1087
-rw-r--r--apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php81
-rw-r--r--apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php101
-rw-r--r--apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php55
-rw-r--r--apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php48
-rw-r--r--apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php67
-rw-r--r--apps/dav/tests/unit/CalDAV/OutboxTest.php105
-rw-r--r--apps/dav/tests/unit/CalDAV/PluginTest.php47
-rw-r--r--apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php141
-rw-r--r--apps/dav/tests/unit/CalDAV/PublicCalendarTest.php151
-rw-r--r--apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php61
-rw-r--r--apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php74
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php377
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php52
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php17
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php509
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php173
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php83
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php263
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php777
-rw-r--r--apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTestCase.php556
-rw-r--r--apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php28
-rw-r--r--apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php28
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php193
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php1080
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php2200
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php770
-rw-r--r--apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php324
-rw-r--r--apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php117
-rw-r--r--apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php188
-rw-r--r--apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php445
-rw-r--r--apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php51
-rw-r--r--apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php142
-rw-r--r--apps/dav/tests/unit/CalDAV/TipBrokerTest.php180
-rw-r--r--apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php73
-rw-r--r--apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php176
-rw-r--r--apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php152
-rw-r--r--apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php325
-rw-r--r--apps/dav/tests/unit/CapabilitiesTest.php81
-rw-r--r--apps/dav/tests/unit/CardDAV/Activity/BackendTest.php483
-rw-r--r--apps/dav/tests/unit/CardDAV/AddressBookImplTest.php537
-rw-r--r--apps/dav/tests/unit/CardDAV/AddressBookTest.php184
-rw-r--r--apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php433
-rw-r--r--apps/dav/tests/unit/CardDAV/CardDavBackendTest.php918
-rw-r--r--apps/dav/tests/unit/CardDAV/ContactsManagerTest.php37
-rw-r--r--apps/dav/tests/unit/CardDAV/ConverterTest.php221
-rw-r--r--apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php174
-rw-r--r--apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php146
-rw-r--r--apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php62
-rw-r--r--apps/dav/tests/unit/CardDAV/SyncServiceTest.php480
-rw-r--r--apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php428
-rw-r--r--apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php73
-rw-r--r--apps/dav/tests/unit/Command/DeleteCalendarTest.php231
-rw-r--r--apps/dav/tests/unit/Command/ListAddressbooksTest.php107
-rw-r--r--apps/dav/tests/unit/Command/ListCalendarSharesTest.php172
-rw-r--r--apps/dav/tests/unit/Command/ListCalendarsTest.php112
-rw-r--r--apps/dav/tests/unit/Command/MoveCalendarTest.php354
-rw-r--r--apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php54
-rw-r--r--apps/dav/tests/unit/Comments/CommentsNodeTest.php (renamed from apps/dav/tests/unit/comments/commentnode.php)332
-rw-r--r--apps/dav/tests/unit/Comments/CommentsPluginTest.php (renamed from apps/dav/tests/unit/comments/commentsplugin.php)407
-rw-r--r--apps/dav/tests/unit/Comments/EntityCollectionTest.php121
-rw-r--r--apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php77
-rw-r--r--apps/dav/tests/unit/Comments/RootCollectionTest.php161
-rw-r--r--apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php230
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/AuthTest.php608
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php82
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php177
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php117
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php78
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php234
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php468
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php57
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php44
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php44
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php66
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php160
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/FileTest.php1031
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php720
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php853
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php52
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/NodeTest.php271
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php243
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php937
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php133
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php92
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php98
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php384
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php152
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/Auth.php (renamed from apps/dav/tests/unit/connector/sabre/requesttest/auth.php)53
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php43
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php57
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php37
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php37
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php32
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php42
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php (renamed from apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php)101
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php57
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php78
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php282
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php410
-rw-r--r--apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php96
-rw-r--r--apps/dav/tests/unit/Controller/DirectControllerTest.php138
-rw-r--r--apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php461
-rw-r--r--apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php73
-rw-r--r--apps/dav/tests/unit/DAV/AnonymousOptionsTest.php92
-rw-r--r--apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php42
-rw-r--r--apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php466
-rw-r--r--apps/dav/tests/unit/DAV/GroupPrincipalTest.php331
-rw-r--r--apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php183
-rw-r--r--apps/dav/tests/unit/DAV/Sharing/BackendTest.php399
-rw-r--r--apps/dav/tests/unit/DAV/Sharing/PluginTest.php62
-rw-r--r--apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php100
-rw-r--r--apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php167
-rw-r--r--apps/dav/tests/unit/Direct/DirectFileTest.php111
-rw-r--r--apps/dav/tests/unit/Direct/DirectHomeTest.php160
-rw-r--r--apps/dav/tests/unit/Files/FileSearchBackendTest.php421
-rw-r--r--apps/dav/tests/unit/Files/MultipartRequestParserTest.php322
-rw-r--r--apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php258
-rw-r--r--apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php80
-rw-r--r--apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php173
-rw-r--r--apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php606
-rw-r--r--apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php230
-rw-r--r--apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php47
-rw-r--r--apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php119
-rw-r--r--apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php78
-rw-r--r--apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php140
-rw-r--r--apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php70
-rw-r--r--apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php240
-rw-r--r--apps/dav/tests/unit/Search/ContactsSearchProviderTest.php259
-rw-r--r--apps/dav/tests/unit/Search/EventsSearchProviderTest.php450
-rw-r--r--apps/dav/tests/unit/Search/TasksSearchProviderTest.php313
-rw-r--r--apps/dav/tests/unit/ServerTest.php42
-rw-r--r--apps/dav/tests/unit/Service/AbsenceServiceTest.php445
-rw-r--r--apps/dav/tests/unit/Service/ExampleContactServiceTest.php194
-rw-r--r--apps/dav/tests/unit/Service/ExampleEventServiceTest.php196
-rw-r--r--apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php89
-rw-r--r--apps/dav/tests/unit/Settings/CalDAVSettingsTest.php88
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php157
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php272
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php664
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php224
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php347
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php151
-rw-r--r--apps/dav/tests/unit/Upload/AssemblyStreamTest.php166
-rw-r--r--apps/dav/tests/unit/Upload/ChunkingPluginTest.php189
-rw-r--r--apps/dav/tests/unit/Upload/FutureFileTest.php95
-rw-r--r--apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php133
-rw-r--r--apps/dav/tests/unit/appinfo/applicationtest.php62
-rw-r--r--apps/dav/tests/unit/bootstrap.php38
-rw-r--r--apps/dav/tests/unit/caldav/caldavbackendtest.php491
-rw-r--r--apps/dav/tests/unit/caldav/calendartest.php166
-rw-r--r--apps/dav/tests/unit/caldav/schedule/imipplugintest.php91
-rw-r--r--apps/dav/tests/unit/carddav/addressbookimpltest.php288
-rw-r--r--apps/dav/tests/unit/carddav/addressbooktest.php139
-rw-r--r--apps/dav/tests/unit/carddav/birthdayservicetest.php171
-rw-r--r--apps/dav/tests/unit/carddav/carddavbackendtest.php631
-rw-r--r--apps/dav/tests/unit/carddav/contactsmanagertest.php43
-rw-r--r--apps/dav/tests/unit/carddav/convertertest.php137
-rw-r--r--apps/dav/tests/unit/carddav/sharing/plugintest.php81
-rw-r--r--apps/dav/tests/unit/carddav/syncservicetest.php142
-rw-r--r--apps/dav/tests/unit/comments/entitycollection.php116
-rw-r--r--apps/dav/tests/unit/comments/entitytypecollection.php97
-rw-r--r--apps/dav/tests/unit/comments/rootcollection.php160
-rw-r--r--apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php130
-rw-r--r--apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php70
-rw-r--r--apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php174
-rw-r--r--apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php73
-rw-r--r--apps/dav/tests/unit/connector/sabre/auth.php604
-rw-r--r--apps/dav/tests/unit/connector/sabre/commentpropertiesplugin.php148
-rw-r--r--apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php62
-rw-r--r--apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php313
-rw-r--r--apps/dav/tests/unit/connector/sabre/directory.php264
-rw-r--r--apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php57
-rw-r--r--apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php58
-rw-r--r--apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php83
-rw-r--r--apps/dav/tests/unit/connector/sabre/file.php987
-rw-r--r--apps/dav/tests/unit/connector/sabre/filesplugin.php419
-rw-r--r--apps/dav/tests/unit/connector/sabre/filesreportplugin.php603
-rw-r--r--apps/dav/tests/unit/connector/sabre/node.php130
-rw-r--r--apps/dav/tests/unit/connector/sabre/objecttree.php355
-rw-r--r--apps/dav/tests/unit/connector/sabre/principal.php258
-rw-r--r--apps/dav/tests/unit/connector/sabre/quotaplugin.php223
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php73
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php46
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php46
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/partfileinrootupload.php56
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/sapi.php75
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php211
-rw-r--r--apps/dav/tests/unit/connector/sabre/sharesplugin.php259
-rw-r--r--apps/dav/tests/unit/connector/sabre/tagsplugin.php417
-rw-r--r--apps/dav/tests/unit/dav/groupprincipaltest.php164
-rw-r--r--apps/dav/tests/unit/dav/sharing/plugintest.php83
-rw-r--r--apps/dav/tests/unit/dav/systemprincipalbackendtest.php131
-rw-r--r--apps/dav/tests/unit/migration/addressbookadaptertest.php129
-rw-r--r--apps/dav/tests/unit/migration/calendar_schema.xml191
-rw-r--r--apps/dav/tests/unit/migration/calendaradaptertest.php131
-rw-r--r--apps/dav/tests/unit/migration/contacts_schema.xml151
-rw-r--r--apps/dav/tests/unit/migration/migrateaddressbooktest.php81
-rw-r--r--apps/dav/tests/unit/migration/migratecalendartest.php85
-rw-r--r--apps/dav/tests/unit/phpunit.xml11
-rw-r--r--apps/dav/tests/unit/servertest.php43
-rw-r--r--apps/dav/tests/unit/systemtag/systemtagmappingnode.php132
-rw-r--r--apps/dav/tests/unit/systemtag/systemtagnode.php244
-rw-r--r--apps/dav/tests/unit/systemtag/systemtagplugin.php608
-rw-r--r--apps/dav/tests/unit/systemtag/systemtagsbyidcollection.php244
-rw-r--r--apps/dav/tests/unit/systemtag/systemtagsobjectmappingcollection.php381
-rw-r--r--apps/dav/tests/unit/systemtag/systemtagsobjecttypecollection.php160
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics17
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics17
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics17
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics17
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics14
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics15
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics14
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics14
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics20
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license2
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-expected.ics18
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license2
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics21
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license2
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event.ics18
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event.ics.license2
259 files changed, 43386 insertions, 12440 deletions
diff --git a/apps/dav/tests/unit/AppInfo/ApplicationTest.php b/apps/dav/tests/unit/AppInfo/ApplicationTest.php
new file mode 100644
index 00000000000..336f487e0b8
--- /dev/null
+++ b/apps/dav/tests/unit/AppInfo/ApplicationTest.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\AppInfo;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\ContactsManager;
+use Test\TestCase;
+
+/**
+ * Class ApplicationTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit\AppInfo
+ */
+class ApplicationTest extends TestCase {
+ public function test(): void {
+ $app = new Application();
+ $c = $app->getContainer();
+
+ // assert service instances in the container are properly setup
+ $s = $c->query(ContactsManager::class);
+ $this->assertInstanceOf(ContactsManager::class, $s);
+ $s = $c->query(CardDavBackend::class);
+ $this->assertInstanceOf(CardDavBackend::class, $s);
+ }
+}
diff --git a/apps/dav/tests/unit/AppInfo/PluginManagerTest.php b/apps/dav/tests/unit/AppInfo/PluginManagerTest.php
new file mode 100644
index 00000000000..0082aa45286
--- /dev/null
+++ b/apps/dav/tests/unit/AppInfo/PluginManagerTest.php
@@ -0,0 +1,126 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 ownCloud GmbH.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\AppInfo;
+
+use OC\App\AppManager;
+use OC\ServerContainer;
+use OCA\DAV\AppInfo\PluginManager;
+use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
+use OCA\DAV\CalDAV\Integration\ICalendarProvider;
+use Sabre\DAV\Collection;
+use Sabre\DAV\ServerPlugin;
+use Test\TestCase;
+
+/**
+ * Class PluginManagerTest
+ *
+ * @package OCA\DAV\Tests\Unit\AppInfo
+ */
+class PluginManagerTest extends TestCase {
+ public function test(): void {
+ $server = $this->createMock(ServerContainer::class);
+
+ $appManager = $this->createMock(AppManager::class);
+ $appManager->method('getEnabledApps')
+ ->willReturn(['adavapp', 'adavapp2']);
+
+ $appInfo1 = [
+ 'types' => ['dav'],
+ 'sabre' => [
+ 'plugins' => [
+ 'plugin' => [
+ '\OCA\DAV\ADavApp\PluginOne',
+ '\OCA\DAV\ADavApp\PluginTwo',
+ ],
+ ],
+ 'calendar-plugins' => [
+ 'plugin' => [
+ '\OCA\DAV\ADavApp\CalendarPluginOne',
+ '\OCA\DAV\ADavApp\CalendarPluginTwo',
+ ],
+ ],
+ 'collections' => [
+ 'collection' => [
+ '\OCA\DAV\ADavApp\CollectionOne',
+ '\OCA\DAV\ADavApp\CollectionTwo',
+ ]
+ ],
+ ],
+ ];
+ $appInfo2 = [
+ 'types' => ['logging', 'dav'],
+ 'sabre' => [
+ 'plugins' => [
+ 'plugin' => '\OCA\DAV\ADavApp2\PluginOne',
+ ],
+ 'calendar-plugins' => [
+ 'plugin' => '\OCA\DAV\ADavApp2\CalendarPluginOne',
+ ],
+ 'collections' => [
+ 'collection' => '\OCA\DAV\ADavApp2\CollectionOne',
+ ],
+ ],
+ ];
+
+ $appManager->method('getAppInfo')
+ ->willReturnMap([
+ ['adavapp', false, null, $appInfo1],
+ ['adavapp2', false, null, $appInfo2],
+ ]);
+
+ $pluginManager = new PluginManager($server, $appManager);
+
+ $appCalendarPlugin = $this->createMock(AppCalendarPlugin::class);
+ $calendarPlugin1 = $this->createMock(ICalendarProvider::class);
+ $calendarPlugin2 = $this->createMock(ICalendarProvider::class);
+ $calendarPlugin3 = $this->createMock(ICalendarProvider::class);
+
+ $dummyPlugin1 = $this->createMock(ServerPlugin::class);
+ $dummyPlugin2 = $this->createMock(ServerPlugin::class);
+ $dummy2Plugin1 = $this->createMock(ServerPlugin::class);
+
+ $dummyCollection1 = $this->createMock(Collection::class);
+ $dummyCollection2 = $this->createMock(Collection::class);
+ $dummy2Collection1 = $this->createMock(Collection::class);
+
+ $server->method('get')
+ ->willReturnMap([
+ [AppCalendarPlugin::class, $appCalendarPlugin],
+ ['\OCA\DAV\ADavApp\PluginOne', $dummyPlugin1],
+ ['\OCA\DAV\ADavApp\PluginTwo', $dummyPlugin2],
+ ['\OCA\DAV\ADavApp\CalendarPluginOne', $calendarPlugin1],
+ ['\OCA\DAV\ADavApp\CalendarPluginTwo', $calendarPlugin2],
+ ['\OCA\DAV\ADavApp\CollectionOne', $dummyCollection1],
+ ['\OCA\DAV\ADavApp\CollectionTwo', $dummyCollection2],
+ ['\OCA\DAV\ADavApp2\PluginOne', $dummy2Plugin1],
+ ['\OCA\DAV\ADavApp2\CalendarPluginOne', $calendarPlugin3],
+ ['\OCA\DAV\ADavApp2\CollectionOne', $dummy2Collection1],
+ ]);
+
+ $expectedPlugins = [
+ $dummyPlugin1,
+ $dummyPlugin2,
+ $dummy2Plugin1,
+ ];
+ $expectedCalendarPlugins = [
+ $appCalendarPlugin,
+ $calendarPlugin1,
+ $calendarPlugin2,
+ $calendarPlugin3,
+ ];
+ $expectedCollections = [
+ $dummyCollection1,
+ $dummyCollection2,
+ $dummy2Collection1,
+ ];
+
+ $this->assertEquals($expectedPlugins, $pluginManager->getAppPlugins());
+ $this->assertEquals($expectedCalendarPlugins, $pluginManager->getCalendarPlugins());
+ $this->assertEquals($expectedCollections, $pluginManager->getAppCollections());
+ }
+}
diff --git a/apps/dav/tests/unit/Avatars/AvatarHomeTest.php b/apps/dav/tests/unit/Avatars/AvatarHomeTest.php
new file mode 100644
index 00000000000..7117637a000
--- /dev/null
+++ b/apps/dav/tests/unit/Avatars/AvatarHomeTest.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Avatars;
+
+use OCA\DAV\Avatars\AvatarHome;
+use OCA\DAV\Avatars\AvatarNode;
+use OCP\IAvatar;
+use OCP\IAvatarManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\MethodNotAllowed;
+use Sabre\DAV\Exception\NotFound;
+use Test\TestCase;
+
+class AvatarHomeTest extends TestCase {
+ private AvatarHome $home;
+ private IAvatarManager&MockObject $avatarManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->avatarManager = $this->createMock(IAvatarManager::class);
+ $this->home = new AvatarHome(['uri' => 'principals/users/admin'], $this->avatarManager);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesForbiddenMethods')]
+ public function testForbiddenMethods($method): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->home->$method('');
+ }
+
+ public static function providesForbiddenMethods(): array {
+ return [
+ ['createFile'],
+ ['createDirectory'],
+ ['delete'],
+ ['setName']
+ ];
+ }
+
+ public function testGetName(): void {
+ $n = $this->home->getName();
+ self::assertEquals('admin', $n);
+ }
+
+ public static function providesTestGetChild(): array {
+ return [
+ [MethodNotAllowed::class, false, ''],
+ [MethodNotAllowed::class, false, 'bla.foo'],
+ [MethodNotAllowed::class, false, 'bla.png'],
+ [NotFound::class, false, '512.png'],
+ [null, true, '512.png'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesTestGetChild')]
+ public function testGetChild(?string $expectedException, bool $hasAvatar, string $path): void {
+ if ($expectedException !== null) {
+ $this->expectException($expectedException);
+ }
+
+ $avatar = $this->createMock(IAvatar::class);
+ $avatar->method('exists')->willReturn($hasAvatar);
+
+ $this->avatarManager->expects($this->any())->method('getAvatar')->with('admin')->willReturn($avatar);
+ $avatarNode = $this->home->getChild($path);
+ $this->assertInstanceOf(AvatarNode::class, $avatarNode);
+ }
+
+ public function testGetChildren(): void {
+ $avatarNodes = $this->home->getChildren();
+ self::assertEquals(0, count($avatarNodes));
+
+ $avatar = $this->createMock(IAvatar::class);
+ $avatar->expects($this->once())->method('exists')->willReturn(true);
+ $this->avatarManager->expects($this->any())->method('getAvatar')->with('admin')->willReturn($avatar);
+ $avatarNodes = $this->home->getChildren();
+ self::assertEquals(1, count($avatarNodes));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesTestGetChild')]
+ public function testChildExists(?string $expectedException, bool $hasAvatar, string $path): void {
+ $avatar = $this->createMock(IAvatar::class);
+ $avatar->method('exists')->willReturn($hasAvatar);
+
+ $this->avatarManager->expects($this->any())->method('getAvatar')->with('admin')->willReturn($avatar);
+ $childExists = $this->home->childExists($path);
+ $this->assertEquals($hasAvatar, $childExists);
+ }
+
+ public function testGetLastModified(): void {
+ self::assertNull($this->home->getLastModified());
+ }
+}
diff --git a/apps/dav/tests/unit/Avatars/AvatarNodeTest.php b/apps/dav/tests/unit/Avatars/AvatarNodeTest.php
new file mode 100644
index 00000000000..0ca147a1f3b
--- /dev/null
+++ b/apps/dav/tests/unit/Avatars/AvatarNodeTest.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Avatars;
+
+use OCA\DAV\Avatars\AvatarNode;
+use OCP\IAvatar;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class AvatarNodeTest extends TestCase {
+ public function testGetName(): void {
+ /** @var IAvatar&MockObject $a */
+ $a = $this->createMock(IAvatar::class);
+ $n = new AvatarNode(1024, 'png', $a);
+ $this->assertEquals('1024.png', $n->getName());
+ }
+
+ public function testGetContentType(): void {
+ /** @var IAvatar&MockObject $a */
+ $a = $this->createMock(IAvatar::class);
+ $n = new AvatarNode(1024, 'png', $a);
+ $this->assertEquals('image/png', $n->getContentType());
+
+ $n = new AvatarNode(1024, 'jpeg', $a);
+ $this->assertEquals('image/jpeg', $n->getContentType());
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php b/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php
new file mode 100644
index 00000000000..b2199e3e657
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OCA\DAV\BackgroundJob\CleanupInvitationTokenJob;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\DB\QueryBuilder\IExpressionBuilder;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class CleanupInvitationTokenJobTest extends TestCase {
+ private IDBConnection&MockObject $dbConnection;
+ private ITimeFactory&MockObject $timeFactory;
+ private CleanupInvitationTokenJob $backgroundJob;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->dbConnection = $this->createMock(IDBConnection::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+
+ $this->backgroundJob = new CleanupInvitationTokenJob(
+ $this->dbConnection, $this->timeFactory);
+ }
+
+ public function testRun(): void {
+ $this->timeFactory->expects($this->once())
+ ->method('getTime')
+ ->with()
+ ->willReturn(1337);
+
+ $queryBuilder = $this->createMock(IQueryBuilder::class);
+ $expr = $this->createMock(IExpressionBuilder::class);
+ $stmt = $this->createMock(\Doctrine\DBAL\Driver\Statement::class);
+
+ $this->dbConnection->expects($this->once())
+ ->method('getQueryBuilder')
+ ->with()
+ ->willReturn($queryBuilder);
+ $queryBuilder->method('expr')
+ ->willReturn($expr);
+ $queryBuilder->method('createNamedParameter')
+ ->willReturnMap([
+ [1337, \PDO::PARAM_STR, null, 'namedParameter1337']
+ ]);
+
+ $function = 'function1337';
+ $expr->expects($this->once())
+ ->method('lt')
+ ->with('expiration', 'namedParameter1337')
+ ->willReturn($function);
+
+ $this->dbConnection->expects($this->once())
+ ->method('getQueryBuilder')
+ ->with()
+ ->willReturn($queryBuilder);
+
+ $queryBuilder->expects($this->once())
+ ->method('delete')
+ ->with('calendar_invitations')
+ ->willReturn($queryBuilder);
+ $queryBuilder->expects($this->once())
+ ->method('where')
+ ->with($function)
+ ->willReturn($queryBuilder);
+ $queryBuilder->expects($this->once())
+ ->method('execute')
+ ->with()
+ ->willReturn($stmt);
+
+ $this->backgroundJob->run([]);
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php b/apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php
new file mode 100644
index 00000000000..2065b8fe946
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php
@@ -0,0 +1,170 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OCA\DAV\BackgroundJob\CleanupOrphanedChildrenJob;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IExpressionBuilder;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class CleanupOrphanedChildrenJobTest extends TestCase {
+ private CleanupOrphanedChildrenJob $job;
+
+ private ITimeFactory&MockObject $timeFactory;
+ private IDBConnection&MockObject $connection;
+ private LoggerInterface&MockObject $logger;
+ private IJobList&MockObject $jobList;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->connection = $this->createMock(IDBConnection::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->jobList = $this->createMock(IJobList::class);
+
+ $this->job = new CleanupOrphanedChildrenJob(
+ $this->timeFactory,
+ $this->connection,
+ $this->logger,
+ $this->jobList,
+ );
+ }
+
+ private function getArgument(): array {
+ return [
+ 'childTable' => 'childTable',
+ 'parentTable' => 'parentTable',
+ 'parentId' => 'parentId',
+ 'logMessage' => 'logMessage',
+ ];
+ }
+
+ private function getMockQueryBuilder(): IQueryBuilder&MockObject {
+ $expr = $this->createMock(IExpressionBuilder::class);
+ $qb = $this->createMock(IQueryBuilder::class);
+ $qb->method('select')
+ ->willReturnSelf();
+ $qb->method('from')
+ ->willReturnSelf();
+ $qb->method('leftJoin')
+ ->willReturnSelf();
+ $qb->method('where')
+ ->willReturnSelf();
+ $qb->method('setMaxResults')
+ ->willReturnSelf();
+ $qb->method('andWhere')
+ ->willReturnSelf();
+ $qb->method('expr')
+ ->willReturn($expr);
+ $qb->method('delete')
+ ->willReturnSelf();
+ return $qb;
+ }
+
+ public function testRunWithoutOrphans(): void {
+ $argument = $this->getArgument();
+ $selectQb = $this->getMockQueryBuilder();
+ $result = $this->createMock(IResult::class);
+
+ $this->connection->expects(self::once())
+ ->method('getQueryBuilder')
+ ->willReturn($selectQb);
+ $selectQb->expects(self::once())
+ ->method('executeQuery')
+ ->willReturn($result);
+ $result->expects(self::once())
+ ->method('fetchAll')
+ ->willReturn([]);
+ $result->expects(self::once())
+ ->method('closeCursor');
+ $this->jobList->expects(self::never())
+ ->method('add');
+
+ self::invokePrivate($this->job, 'run', [$argument]);
+ }
+
+ public function testRunWithPartialBatch(): void {
+ $argument = $this->getArgument();
+ $selectQb = $this->getMockQueryBuilder();
+ $deleteQb = $this->getMockQueryBuilder();
+ $result = $this->createMock(IResult::class);
+
+ $calls = [
+ $selectQb,
+ $deleteQb,
+ ];
+ $this->connection->method('getQueryBuilder')
+ ->willReturnCallback(function () use (&$calls) {
+ return array_shift($calls);
+ });
+ $selectQb->expects(self::once())
+ ->method('executeQuery')
+ ->willReturn($result);
+ $result->expects(self::once())
+ ->method('fetchAll')
+ ->willReturn([
+ ['id' => 42],
+ ['id' => 43],
+ ]);
+ $result->expects(self::once())
+ ->method('closeCursor');
+ $deleteQb->expects(self::once())
+ ->method('delete')
+ ->willReturnSelf();
+ $deleteQb->expects(self::once())
+ ->method('executeStatement');
+ $this->jobList->expects(self::never())
+ ->method('add');
+
+ self::invokePrivate($this->job, 'run', [$argument]);
+ }
+
+ public function testRunWithFullBatch(): void {
+ $argument = $this->getArgument();
+ $selectQb = $this->getMockQueryBuilder();
+ $deleteQb = $this->getMockQueryBuilder();
+ $result = $this->createMock(IResult::class);
+
+ $calls = [
+ $selectQb,
+ $deleteQb,
+ ];
+ $this->connection->method('getQueryBuilder')
+ ->willReturnCallback(function () use (&$calls) {
+ return array_shift($calls);
+ });
+
+ $selectQb->expects(self::once())
+ ->method('executeQuery')
+ ->willReturn($result);
+ $result->expects(self::once())
+ ->method('fetchAll')
+ ->willReturn(array_map(static fn ($i) => ['id' => 42 + $i], range(0, 999)));
+ $result->expects(self::once())
+ ->method('closeCursor');
+ $deleteQb->expects(self::once())
+ ->method('delete')
+ ->willReturnSelf();
+ $deleteQb->expects(self::once())
+ ->method('executeStatement');
+ $this->jobList->expects(self::once())
+ ->method('add')
+ ->with(CleanupOrphanedChildrenJob::class, $argument);
+
+ self::invokePrivate($this->job, 'run', [$argument]);
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php b/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php
new file mode 100644
index 00000000000..a46a1e5e5b0
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OCA\DAV\BackgroundJob\EventReminderJob;
+use OCA\DAV\CalDAV\Reminder\ReminderService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class EventReminderJobTest extends TestCase {
+ private ITimeFactory&MockObject $time;
+ private ReminderService&MockObject $reminderService;
+ private IConfig&MockObject $config;
+ private EventReminderJob $backgroundJob;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->time = $this->createMock(ITimeFactory::class);
+ $this->reminderService = $this->createMock(ReminderService::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $this->backgroundJob = new EventReminderJob(
+ $this->time,
+ $this->reminderService,
+ $this->config,
+ );
+ }
+
+ public static function data(): array {
+ return [
+ [true, true, true],
+ [true, false, false],
+ [false, true, false],
+ [false, false, false],
+ ];
+ }
+
+ /**
+ *
+ * @param bool $sendEventReminders
+ * @param bool $sendEventRemindersMode
+ * @param bool $expectCall
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('data')]
+ public function testRun(bool $sendEventReminders, bool $sendEventRemindersMode, bool $expectCall): void {
+ $this->config->expects($this->exactly($sendEventReminders ? 2 : 1))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['dav', 'sendEventReminders', 'yes', ($sendEventReminders ? 'yes' : 'no')],
+ ['dav', 'sendEventRemindersMode', 'backgroundjob', ($sendEventRemindersMode ? 'backgroundjob' : 'cron')],
+ ]);
+
+ if ($expectCall) {
+ $this->reminderService->expects($this->once())
+ ->method('processReminders');
+ } else {
+ $this->reminderService->expects($this->never())
+ ->method('processReminders');
+ }
+
+ $this->backgroundJob->run([]);
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php b/apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php
new file mode 100644
index 00000000000..88a76ae1332
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php
@@ -0,0 +1,113 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob;
+use OCA\DAV\CalDAV\BirthdayService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class GenerateBirthdayCalendarBackgroundJobTest extends TestCase {
+ private ITimeFactory&MockObject $time;
+ private BirthdayService&MockObject $birthdayService;
+ private IConfig&MockObject $config;
+ private GenerateBirthdayCalendarBackgroundJob $backgroundJob;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->time = $this->createMock(ITimeFactory::class);
+ $this->birthdayService = $this->createMock(BirthdayService::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $this->backgroundJob = new GenerateBirthdayCalendarBackgroundJob(
+ $this->time,
+ $this->birthdayService,
+ $this->config,
+ );
+ }
+
+ public function testRun(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('yes');
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('yes');
+
+ $this->birthdayService->expects($this->never())
+ ->method('resetForUser')
+ ->with('user123');
+
+ $this->birthdayService->expects($this->once())
+ ->method('syncUser')
+ ->with('user123');
+
+ $this->backgroundJob->run(['userId' => 'user123']);
+ }
+
+ public function testRunAndReset(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('yes');
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('yes');
+
+ $this->birthdayService->expects($this->once())
+ ->method('resetForUser')
+ ->with('user123');
+
+ $this->birthdayService->expects($this->once())
+ ->method('syncUser')
+ ->with('user123');
+
+ $this->backgroundJob->run(['userId' => 'user123', 'purgeBeforeGenerating' => true]);
+ }
+
+ public function testRunGloballyDisabled(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('no');
+
+ $this->config->expects($this->never())
+ ->method('getUserValue');
+
+ $this->birthdayService->expects($this->never())
+ ->method('syncUser');
+
+ $this->backgroundJob->run(['userId' => 'user123']);
+ }
+
+ public function testRunUserDisabled(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('yes');
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('no');
+
+ $this->birthdayService->expects($this->never())
+ ->method('syncUser');
+
+ $this->backgroundJob->run(['userId' => 'user123']);
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php b/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php
new file mode 100644
index 00000000000..6135fd00fdc
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php
@@ -0,0 +1,148 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Db\Absence;
+use OCA\DAV\Db\AbsenceMapper;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\User\Events\OutOfOfficeEndedEvent;
+use OCP\User\Events\OutOfOfficeStartedEvent;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class OutOfOfficeEventDispatcherJobTest extends TestCase {
+ private OutOfOfficeEventDispatcherJob $job;
+ private ITimeFactory&MockObject $timeFactory;
+ private AbsenceMapper&MockObject $absenceMapper;
+ private LoggerInterface&MockObject $logger;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private IUserManager&MockObject $userManager;
+ private MockObject|TimezoneService $timezoneService;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->absenceMapper = $this->createMock(AbsenceMapper::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->timezoneService = $this->createMock(TimezoneService::class);
+
+ $this->job = new OutOfOfficeEventDispatcherJob(
+ $this->timeFactory,
+ $this->absenceMapper,
+ $this->logger,
+ $this->eventDispatcher,
+ $this->userManager,
+ $this->timezoneService,
+ );
+ }
+
+ public function testDispatchStartEvent(): void {
+ $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin');
+
+ $absence = new Absence();
+ $absence->setId(200);
+ $absence->setUserId('user');
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findById')
+ ->with(1)
+ ->willReturn($absence);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with('user')
+ ->willReturn($user);
+ $this->eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::callback(static function ($event): bool {
+ self::assertInstanceOf(OutOfOfficeStartedEvent::class, $event);
+ return true;
+ }));
+
+ $this->job->run([
+ 'id' => 1,
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+ ]);
+ }
+
+ public function testDispatchStopEvent(): void {
+ $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin');
+
+ $absence = new Absence();
+ $absence->setId(200);
+ $absence->setUserId('user');
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findById')
+ ->with(1)
+ ->willReturn($absence);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with('user')
+ ->willReturn($user);
+ $this->eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::callback(static function ($event): bool {
+ self::assertInstanceOf(OutOfOfficeEndedEvent::class, $event);
+ return true;
+ }));
+
+ $this->job->run([
+ 'id' => 1,
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ]);
+ }
+
+ public function testDoesntDispatchUnknownEvent(): void {
+ $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin');
+
+ $absence = new Absence();
+ $absence->setId(100);
+ $absence->setUserId('user');
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findById')
+ ->with(1)
+ ->willReturn($absence);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with('user')
+ ->willReturn($user);
+ $this->eventDispatcher->expects(self::never())
+ ->method('dispatchTyped');
+ $this->logger->expects(self::once())
+ ->method('error');
+
+ $this->job->run([
+ 'id' => 1,
+ 'event' => 'foobar',
+ ]);
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php b/apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php
new file mode 100644
index 00000000000..1838fb2537d
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use InvalidArgumentException;
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\BackgroundJob\PruneOutdatedSyncTokensJob;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class PruneOutdatedSyncTokensJobTest extends TestCase {
+ private ITimeFactory&MockObject $timeFactory;
+ private CalDavBackend&MockObject $calDavBackend;
+ private CardDavBackend&MockObject $cardDavBackend;
+ private IConfig&MockObject $config;
+ private LoggerInterface&MockObject $logger;
+ private PruneOutdatedSyncTokensJob $backgroundJob;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->calDavBackend = $this->createMock(CalDavBackend::class);
+ $this->cardDavBackend = $this->createMock(CardDavBackend::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->backgroundJob = new PruneOutdatedSyncTokensJob($this->timeFactory, $this->calDavBackend, $this->cardDavBackend, $this->config, $this->logger);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestRun')]
+ public function testRun(string $configToKeep, string $configRetentionDays, int $actualLimit, int $retentionDays, int $deletedCalendarSyncTokens, int $deletedAddressBookSyncTokens): void {
+ $this->config->expects($this->exactly(2))
+ ->method('getAppValue')
+ ->with(Application::APP_ID, self::anything(), self::anything())
+ ->willReturnCallback(function ($app, $key) use ($configToKeep, $configRetentionDays) {
+ switch ($key) {
+ case 'totalNumberOfSyncTokensToKeep':
+ return $configToKeep;
+ case 'syncTokensRetentionDays':
+ return $configRetentionDays;
+ default:
+ throw new InvalidArgumentException();
+ }
+ });
+ $this->calDavBackend->expects($this->once())
+ ->method('pruneOutdatedSyncTokens')
+ ->with($actualLimit)
+ ->willReturn($deletedCalendarSyncTokens);
+ $this->cardDavBackend->expects($this->once())
+ ->method('pruneOutdatedSyncTokens')
+ ->with($actualLimit, $retentionDays)
+ ->willReturn($deletedAddressBookSyncTokens);
+ $this->logger->expects($this->once())
+ ->method('info')
+ ->with('Pruned {calendarSyncTokensNumber} calendar sync tokens and {addressBooksSyncTokensNumber} address book sync tokens', [
+ 'calendarSyncTokensNumber' => $deletedCalendarSyncTokens,
+ 'addressBooksSyncTokensNumber' => $deletedAddressBookSyncTokens
+ ]);
+
+ $this->backgroundJob->run(null);
+ }
+
+ public static function dataForTestRun(): array {
+ return [
+ ['100', '2', 100, 7 * 24 * 3600, 2, 3],
+ ['100', '14', 100, 14 * 24 * 3600, 2, 3],
+ ['0', '60', 1, 60 * 24 * 3600, 0, 0]
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php b/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php
new file mode 100644
index 00000000000..7713ef2945a
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OCA\DAV\BackgroundJob\RefreshWebcalJob;
+use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+use Test\TestCase;
+
+class RefreshWebcalJobTest extends TestCase {
+ private RefreshWebcalService&MockObject $refreshWebcalService;
+ private IConfig&MockObject $config;
+ private LoggerInterface $logger;
+ private ITimeFactory&MockObject $timeFactory;
+ private IJobList&MockObject $jobList;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->refreshWebcalService = $this->createMock(RefreshWebcalService::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+
+ $this->jobList = $this->createMock(IJobList::class);
+ }
+
+ /**
+ *
+ * @param int $lastRun
+ * @param int $time
+ * @param bool $process
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
+ public function testRun(int $lastRun, int $time, bool $process): void {
+ $backgroundJob = new RefreshWebcalJob($this->refreshWebcalService, $this->config, $this->logger, $this->timeFactory);
+ $backgroundJob->setId(42);
+
+ $backgroundJob->setArgument([
+ 'principaluri' => 'principals/users/testuser',
+ 'uri' => 'sub123',
+ ]);
+ $backgroundJob->setLastRun($lastRun);
+
+ $this->refreshWebcalService->expects($this->once())
+ ->method('getSubscription')
+ ->with('principals/users/testuser', 'sub123')
+ ->willReturn([
+ 'id' => '99',
+ 'uri' => 'sub456',
+ '{http://apple.com/ns/ical/}refreshrate' => 'P1D',
+ '{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
+ '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
+ '{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
+ 'source' => 'webcal://foo.bar/bla'
+ ]);
+
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'calendarSubscriptionRefreshRate', 'P1D')
+ ->willReturn('P1W');
+
+ $this->timeFactory->method('getTime')
+ ->willReturn($time);
+
+ if ($process) {
+ $this->refreshWebcalService->expects($this->once())
+ ->method('refreshSubscription')
+ ->with('principals/users/testuser', 'sub123');
+ } else {
+ $this->refreshWebcalService->expects($this->never())
+ ->method('refreshSubscription')
+ ->with('principals/users/testuser', 'sub123');
+ }
+
+ $backgroundJob->start($this->jobList);
+ }
+
+ public static function runDataProvider():array {
+ return [
+ [0, 100000, true],
+ [100000, 100000, false]
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php b/apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php
new file mode 100644
index 00000000000..6c9214d0268
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob;
+use OCA\DAV\BackgroundJob\RegisterRegenerateBirthdayCalendars;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class RegisterRegenerateBirthdayCalendarsTest extends TestCase {
+ private ITimeFactory&MockObject $time;
+ private IUserManager&MockObject $userManager;
+ private IJobList&MockObject $jobList;
+ private RegisterRegenerateBirthdayCalendars $backgroundJob;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->time = $this->createMock(ITimeFactory::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->jobList = $this->createMock(IJobList::class);
+
+ $this->backgroundJob = new RegisterRegenerateBirthdayCalendars(
+ $this->time,
+ $this->userManager,
+ $this->jobList
+ );
+ }
+
+ public function testRun(): void {
+ $this->userManager->expects($this->once())
+ ->method('callForSeenUsers')
+ ->willReturnCallback(function ($closure): void {
+ $user1 = $this->createMock(IUser::class);
+ $user1->method('getUID')->willReturn('uid1');
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')->willReturn('uid2');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')->willReturn('uid3');
+
+ $closure($user1);
+ $closure($user2);
+ $closure($user3);
+ });
+
+ $calls = [
+ 'uid1',
+ 'uid2',
+ 'uid3',
+ ];
+ $this->jobList->expects($this->exactly(3))
+ ->method('add')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals(
+ [
+ GenerateBirthdayCalendarBackgroundJob::class,
+ [
+ 'userId' => $expected,
+ 'purgeBeforeGenerating' => true
+ ]
+ ],
+ func_get_args()
+ );
+ });
+
+ $this->backgroundJob->run([]);
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php b/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php
new file mode 100644
index 00000000000..38a981787cd
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Calendar\Resource\IManager as IResourceManager;
+use OCP\Calendar\Room\IManager as IRoomManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class UpdateCalendarResourcesRoomsBackgroundJobTest extends TestCase {
+ private UpdateCalendarResourcesRoomsBackgroundJob $backgroundJob;
+ private ITimeFactory&MockObject $time;
+ private IResourceManager&MockObject $resourceManager;
+ private IRoomManager&MockObject $roomManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->time = $this->createMock(ITimeFactory::class);
+ $this->resourceManager = $this->createMock(IResourceManager::class);
+ $this->roomManager = $this->createMock(IRoomManager::class);
+
+ $this->backgroundJob = new UpdateCalendarResourcesRoomsBackgroundJob(
+ $this->time,
+ $this->resourceManager,
+ $this->roomManager,
+ );
+ }
+
+ public function testRun(): void {
+ $this->resourceManager->expects(self::once())
+ ->method('update');
+ $this->roomManager->expects(self::once())
+ ->method('update');
+
+ $this->backgroundJob->run([]);
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php
new file mode 100644
index 00000000000..d49d20180d9
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php
@@ -0,0 +1,220 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OC\User\OutOfOfficeData;
+use OCA\DAV\BackgroundJob\UserStatusAutomation;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Server;
+use OCP\User\IAvailabilityCoordinator;
+use OCP\UserStatus\IManager;
+use OCP\UserStatus\IUserStatus;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class UserStatusAutomationTest extends TestCase {
+ protected ITimeFactory&MockObject $time;
+ protected IJobList&MockObject $jobList;
+ protected LoggerInterface&MockObject $logger;
+ protected IManager&MockObject $statusManager;
+ protected IConfig&MockObject $config;
+ private IAvailabilityCoordinator&MockObject $coordinator;
+ private IUserManager&MockObject $userManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->time = $this->createMock(ITimeFactory::class);
+ $this->jobList = $this->createMock(IJobList::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->statusManager = $this->createMock(IManager::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->coordinator = $this->createMock(IAvailabilityCoordinator::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+
+ }
+
+ protected function getAutomationMock(array $methods): MockObject|UserStatusAutomation {
+ if (empty($methods)) {
+ return new UserStatusAutomation(
+ $this->time,
+ Server::get(IDBConnection::class),
+ $this->jobList,
+ $this->logger,
+ $this->statusManager,
+ $this->config,
+ $this->coordinator,
+ $this->userManager,
+ );
+ }
+
+ return $this->getMockBuilder(UserStatusAutomation::class)
+ ->setConstructorArgs([
+ $this->time,
+ Server::get(IDBConnection::class),
+ $this->jobList,
+ $this->logger,
+ $this->statusManager,
+ $this->config,
+ $this->coordinator,
+ $this->userManager,
+ ])
+ ->onlyMethods($methods)
+ ->getMock();
+ }
+
+ public static function dataRun(): array {
+ return [
+ ['20230217', '2023-02-24 10:49:36.613834', true],
+ ['20230224', '2023-02-24 10:49:36.613834', true],
+ ['20230217', '2023-02-24 13:58:24.479357', false],
+ ['20230224', '2023-02-24 13:58:24.479357', false],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataRun')]
+ public function testRunNoOOO(string $ruleDay, string $currentTime, bool $isAvailable): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'user'
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->coordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->config->method('getUserValue')
+ ->with('user', 'dav', 'user_status_automation', 'no')
+ ->willReturn('yes');
+ $this->time->method('getDateTime')
+ ->willReturn(new \DateTime($currentTime, new \DateTimeZone('UTC')));
+ $this->logger->expects(self::exactly(4))
+ ->method('debug');
+ if (!$isAvailable) {
+ $this->statusManager->expects(self::once())
+ ->method('setUserStatus')
+ ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true);
+ }
+ $automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']);
+ $automation->method('getAvailabilityFromPropertiesTable')
+ ->with('user')
+ ->willReturn('BEGIN:VCALENDAR
+PRODID:Nextcloud DAV app
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+BEGIN:DAYLIGHT
+TZNAME:CEST
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VAVAILABILITY
+BEGIN:AVAILABLE
+DTSTART;TZID=Europe/Berlin:' . $ruleDay . 'T090000
+DTEND;TZID=Europe/Berlin:' . $ruleDay . 'T170000
+UID:3e6feeec-8e00-4265-b822-b73174e8b39f
+RRULE:FREQ=WEEKLY;BYDAY=TH
+END:AVAILABLE
+BEGIN:AVAILABLE
+DTSTART;TZID=Europe/Berlin:' . $ruleDay . 'T090000
+DTEND;TZID=Europe/Berlin:' . $ruleDay . 'T120000
+UID:8a634e99-07cf-443b-b480-005a0e1db323
+RRULE:FREQ=WEEKLY;BYDAY=FR
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR');
+
+ self::invokePrivate($automation, 'run', [['userId' => 'user']]);
+ }
+
+ public function testRunNoAvailabilityNoOOO(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'user'
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->coordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->config->method('getUserValue')
+ ->with('user', 'dav', 'user_status_automation', 'no')
+ ->willReturn('yes');
+ $this->time->method('getDateTime')
+ ->willReturn(new \DateTime('2023-02-24 13:58:24.479357', new \DateTimeZone('UTC')));
+ $this->jobList->expects($this->once())
+ ->method('remove')
+ ->with(UserStatusAutomation::class, ['userId' => 'user']);
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->logger->expects(self::once())
+ ->method('info');
+ $automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']);
+ $automation->method('getAvailabilityFromPropertiesTable')
+ ->with('user')
+ ->willReturn(false);
+
+ self::invokePrivate($automation, 'run', [['userId' => 'user']]);
+ }
+
+ public function testRunNoAvailabilityWithOOO(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'user'
+ ]);
+ $ooo = $this->createConfiguredMock(OutOfOfficeData::class, [
+ 'getShortMessage' => 'On Vacation',
+ 'getEndDate' => 123456,
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->coordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn($ooo);
+ $this->coordinator->expects(self::once())
+ ->method('isInEffect')
+ ->willReturn(true);
+ $this->statusManager->expects(self::once())
+ ->method('setUserStatus')
+ ->with('user', IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND, true, $ooo->getShortMessage());
+ $this->config->expects(self::never())
+ ->method('getUserValue');
+ $this->time->method('getDateTime')
+ ->willReturn(new \DateTime('2023-02-24 13:58:24.479357', new \DateTimeZone('UTC')));
+ $this->jobList->expects($this->never())
+ ->method('remove');
+ $this->logger->expects(self::exactly(2))
+ ->method('debug');
+ $automation = $this->getAutomationMock([]);
+
+ self::invokePrivate($automation, 'run', [['userId' => 'user']]);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php
new file mode 100644
index 00000000000..45937d86873
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php
@@ -0,0 +1,263 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OC\KnownUser\KnownUserService;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\CalDAV\Sharing\Backend as SharingBackend;
+use OCA\DAV\CalDAV\Sharing\Service;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCP\Accounts\IAccountManager;
+use OCP\App\IAppManager;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\ICacheFactory;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Security\ISecureRandom;
+use OCP\Server;
+use OCP\Share\IManager as ShareManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
+use Sabre\DAV\Xml\Property\Href;
+use Test\TestCase;
+
+/**
+ * Class CalDavBackendTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\CalDAV
+ */
+abstract class AbstractCalDavBackend extends TestCase {
+
+
+ protected CalDavBackend $backend;
+ protected Principal&MockObject $principal;
+ protected IUserManager&MockObject $userManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IEventDispatcher&MockObject $dispatcher;
+ private LoggerInterface&MockObject $logger;
+ private IConfig&MockObject $config;
+ private ISecureRandom $random;
+ protected SharingBackend $sharingBackend;
+ protected IDBConnection $db;
+ public const UNIT_TEST_USER = 'principals/users/caldav-unit-test';
+ public const UNIT_TEST_USER1 = 'principals/users/caldav-unit-test1';
+ public const UNIT_TEST_GROUP = 'principals/groups/caldav-unit-test-group';
+ public const UNIT_TEST_GROUP2 = 'principals/groups/caldav-unit-test-group2';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->dispatcher = $this->createMock(IEventDispatcher::class);
+ $this->principal = $this->getMockBuilder(Principal::class)
+ ->setConstructorArgs([
+ $this->userManager,
+ $this->groupManager,
+ $this->createMock(IAccountManager::class),
+ $this->createMock(ShareManager::class),
+ $this->createMock(IUserSession::class),
+ $this->createMock(IAppManager::class),
+ $this->createMock(ProxyMapper::class),
+ $this->createMock(KnownUserService::class),
+ $this->createMock(IConfig::class),
+ $this->createMock(IFactory::class)
+ ])
+ ->onlyMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri'])
+ ->getMock();
+ $this->principal->expects($this->any())->method('getPrincipalByPath')
+ ->willReturn([
+ 'uri' => 'principals/best-friend',
+ '{DAV:}displayname' => 'User\'s displayname',
+ ]);
+ $this->principal->expects($this->any())->method('getGroupMembership')
+ ->withAnyParameters()
+ ->willReturn([self::UNIT_TEST_GROUP, self::UNIT_TEST_GROUP2]);
+
+ $this->db = Server::get(IDBConnection::class);
+ $this->random = Server::get(ISecureRandom::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->sharingBackend = new SharingBackend(
+ $this->userManager,
+ $this->groupManager,
+ $this->principal,
+ $this->createMock(ICacheFactory::class),
+ new Service(new SharingMapper($this->db)),
+ $this->logger);
+ $this->backend = new CalDavBackend(
+ $this->db,
+ $this->principal,
+ $this->userManager,
+ $this->random,
+ $this->logger,
+ $this->dispatcher,
+ $this->config,
+ $this->sharingBackend,
+ false,
+ );
+
+ $this->cleanUpBackend();
+ }
+
+ protected function tearDown(): void {
+ $this->cleanUpBackend();
+ parent::tearDown();
+ }
+
+ public function cleanUpBackend(): void {
+ if (is_null($this->backend)) {
+ return;
+ }
+ $this->principal->expects($this->any())->method('getGroupMembership')
+ ->withAnyParameters()
+ ->willReturn([self::UNIT_TEST_GROUP, self::UNIT_TEST_GROUP2]);
+ $this->cleanupForPrincipal(self::UNIT_TEST_USER);
+ $this->cleanupForPrincipal(self::UNIT_TEST_USER1);
+ }
+
+ private function cleanupForPrincipal($principal): void {
+ $calendars = $this->backend->getCalendarsForUser($principal);
+ $this->dispatcher->expects(self::any())
+ ->method('dispatchTyped');
+ foreach ($calendars as $calendar) {
+ $this->backend->deleteCalendar($calendar['id'], true);
+ }
+ $subscriptions = $this->backend->getSubscriptionsForUser($principal);
+ foreach ($subscriptions as $subscription) {
+ $this->backend->deleteSubscription($subscription['id']);
+ }
+ }
+
+ protected function createTestCalendar(): int {
+ $this->dispatcher->expects(self::any())
+ ->method('dispatchTyped');
+
+ $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', [
+ '{http://apple.com/ns/ical/}calendar-color' => '#1C4587FF'
+ ]);
+ $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($calendars));
+ $this->assertEquals(self::UNIT_TEST_USER, $calendars[0]['principaluri']);
+ /** @var SupportedCalendarComponentSet $components */
+ $components = $calendars[0]['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'];
+ $this->assertEquals(['VEVENT','VTODO','VJOURNAL'], $components->getValue());
+ $color = $calendars[0]['{http://apple.com/ns/ical/}calendar-color'];
+ $this->assertEquals('#1C4587FF', $color);
+ $this->assertEquals('Example', $calendars[0]['uri']);
+ $this->assertEquals('Example', $calendars[0]['{DAV:}displayname']);
+ return (int)$calendars[0]['id'];
+ }
+
+ protected function createTestSubscription() {
+ $this->backend->createSubscription(self::UNIT_TEST_USER, 'Example', [
+ '{http://apple.com/ns/ical/}calendar-color' => '#1C4587FF',
+ '{http://calendarserver.org/ns/}source' => new Href(['foo']),
+ ]);
+ $calendars = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($calendars));
+ $this->assertEquals(self::UNIT_TEST_USER, $calendars[0]['principaluri']);
+ $this->assertEquals('Example', $calendars[0]['uri']);
+ $calendarId = $calendars[0]['id'];
+
+ return $calendarId;
+ }
+
+ protected function createEvent($calendarId, $start = '20130912T130000Z', $end = '20130912T140000Z') {
+ $randomPart = self::getUniqueID();
+
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-$randomPart
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:$start
+DTEND;VALUE=DATE-TIME:$end
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $uri0 = $this->getUniqueID('event');
+
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+
+ $this->backend->createCalendarObject($calendarId, $uri0, $calData);
+
+ return $uri0;
+ }
+
+ protected function modifyEvent($calendarId, $objectId, $start = '20130912T130000Z', $end = '20130912T140000Z') {
+ $randomPart = self::getUniqueID();
+
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-$randomPart
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:$start
+DTEND;VALUE=DATE-TIME:$end
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $this->backend->updateCalendarObject($calendarId, $objectId, $calData);
+ }
+
+ protected function deleteEvent($calendarId, $objectId) {
+ $this->backend->deleteCalendarObject($calendarId, $objectId);
+ }
+
+ protected function assertAcl($principal, $privilege, $acl) {
+ foreach ($acl as $a) {
+ if ($a['principal'] === $principal && $a['privilege'] === $privilege) {
+ $this->addToAssertionCount(1);
+ return;
+ }
+ }
+ $this->fail("ACL does not contain $principal / $privilege");
+ }
+
+ protected function assertNotAcl($principal, $privilege, $acl) {
+ foreach ($acl as $a) {
+ if ($a['principal'] === $principal && $a['privilege'] === $privilege) {
+ $this->fail("ACL contains $principal / $privilege");
+ return;
+ }
+ }
+ $this->addToAssertionCount(1);
+ }
+
+ protected function assertAccess($shouldHaveAcl, $principal, $privilege, $acl) {
+ if ($shouldHaveAcl) {
+ $this->assertAcl($principal, $privilege, $acl);
+ } else {
+ $this->assertNotAcl($principal, $privilege, $acl);
+ }
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php b/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php
new file mode 100644
index 00000000000..4848a01f6b9
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php
@@ -0,0 +1,349 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Activity;
+
+use OCA\DAV\CalDAV\Activity\Backend;
+use OCA\DAV\CalDAV\Activity\Provider\Calendar;
+use OCP\Activity\IEvent;
+use OCP\Activity\IManager;
+use OCP\App\IAppManager;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class BackendTest extends TestCase {
+ protected IManager&MockObject $activityManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IUserSession&MockObject $userSession;
+ protected IAppManager&MockObject $appManager;
+ protected IUserManager&MockObject $userManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->activityManager = $this->createMock(IManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ }
+
+ /**
+ * @return Backend|(Backend&MockObject)
+ */
+ protected function getBackend(array $methods = []): Backend {
+ if (empty($methods)) {
+ return new Backend(
+ $this->activityManager,
+ $this->groupManager,
+ $this->userSession,
+ $this->appManager,
+ $this->userManager
+ );
+ } else {
+ return $this->getMockBuilder(Backend::class)
+ ->setConstructorArgs([
+ $this->activityManager,
+ $this->groupManager,
+ $this->userSession,
+ $this->appManager,
+ $this->userManager
+ ])
+ ->onlyMethods($methods)
+ ->getMock();
+ }
+ }
+
+ public static function dataCallTriggerCalendarActivity(): array {
+ return [
+ ['onCalendarAdd', [['data']], Calendar::SUBJECT_ADD, [['data'], [], []]],
+ ['onCalendarUpdate', [['data'], ['shares'], ['changed-properties']], Calendar::SUBJECT_UPDATE, [['data'], ['shares'], ['changed-properties']]],
+ ['onCalendarDelete', [['data'], ['shares']], Calendar::SUBJECT_DELETE, [['data'], ['shares'], []]],
+ ['onCalendarPublication', [['data'], true], Calendar::SUBJECT_PUBLISH, [['data'], [], []]],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCallTriggerCalendarActivity')]
+ public function testCallTriggerCalendarActivity(string $method, array $payload, string $expectedSubject, array $expectedPayload): void {
+ $backend = $this->getBackend(['triggerCalendarActivity']);
+ $backend->expects($this->once())
+ ->method('triggerCalendarActivity')
+ ->willReturnCallback(function () use ($expectedPayload, $expectedSubject): void {
+ $arguments = func_get_args();
+ $this->assertSame($expectedSubject, array_shift($arguments));
+ $this->assertEquals($expectedPayload, $arguments);
+ });
+
+ call_user_func_array([$backend, $method], $payload);
+ }
+
+ public static function dataTriggerCalendarActivity(): array {
+ return [
+ // Add calendar
+ [Calendar::SUBJECT_ADD, [], [], [], '', '', null, []],
+ [Calendar::SUBJECT_ADD, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], [], [], '', 'admin', null, ['admin']],
+ [Calendar::SUBJECT_ADD, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], [], [], 'test2', 'test2', null, ['admin']],
+
+ // Update calendar
+ [Calendar::SUBJECT_UPDATE, [], [], [], '', '', null, []],
+ // No visible change - owner only
+ [Calendar::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], ['shares'], [], '', 'admin', null, ['admin']],
+ // Visible change
+ [Calendar::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], ['shares'], ['{DAV:}displayname' => 'Name'], '', 'admin', ['user1'], ['user1', 'admin']],
+ [Calendar::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], ['shares'], ['{DAV:}displayname' => 'Name'], 'test2', 'test2', ['user1'], ['user1', 'admin']],
+
+ // Delete calendar
+ [Calendar::SUBJECT_DELETE, [], [], [], '', '', null, []],
+ [Calendar::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], ['shares'], [], '', 'admin', [], ['admin']],
+ [Calendar::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], ['shares'], [], '', 'admin', ['user1'], ['user1', 'admin']],
+ [Calendar::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], ['shares'], [], 'test2', 'test2', ['user1'], ['user1', 'admin']],
+
+ // Publish calendar
+ [Calendar::SUBJECT_PUBLISH, [], [], [], '', '', null, []],
+ [Calendar::SUBJECT_PUBLISH, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], ['shares'], [], '', 'admin', [], ['admin']],
+
+ // Unpublish calendar
+ [Calendar::SUBJECT_UNPUBLISH, [], [], [], '', '', null, []],
+ [Calendar::SUBJECT_UNPUBLISH, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], ['shares'], [], '', 'admin', [], ['admin']],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTriggerCalendarActivity')]
+ public function testTriggerCalendarActivity(string $action, array $data, array $shares, array $changedProperties, string $currentUser, string $author, ?array $shareUsers, array $users): void {
+ $backend = $this->getBackend(['getUsersForShares']);
+
+ if ($shareUsers === null) {
+ $backend->expects($this->never())
+ ->method('getUsersForShares');
+ } else {
+ $backend->expects($this->once())
+ ->method('getUsersForShares')
+ ->with($shares)
+ ->willReturn($shareUsers);
+ }
+
+ if ($author !== '') {
+ if ($currentUser !== '') {
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->getUserMock($currentUser));
+ } else {
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn(null);
+ }
+
+ $event = $this->createMock(IEvent::class);
+ $this->activityManager->expects($this->once())
+ ->method('generateEvent')
+ ->willReturn($event);
+
+ $event->expects($this->once())
+ ->method('setApp')
+ ->with('dav')
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setObject')
+ ->with('calendar', $data['id'])
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setType')
+ ->with('calendar')
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setAuthor')
+ ->with($author)
+ ->willReturnSelf();
+
+ $this->userManager->expects($action === Calendar::SUBJECT_DELETE ? $this->exactly(sizeof($users)) : $this->never())
+ ->method('userExists')
+ ->willReturn(true);
+
+ $event->expects($this->exactly(sizeof($users)))
+ ->method('setAffectedUser')
+ ->willReturnSelf();
+ $event->expects($this->exactly(sizeof($users)))
+ ->method('setSubject')
+ ->willReturnSelf();
+ $this->activityManager->expects($this->exactly(sizeof($users)))
+ ->method('publish')
+ ->with($event);
+ } else {
+ $this->activityManager->expects($this->never())
+ ->method('generateEvent');
+ }
+
+ $this->invokePrivate($backend, 'triggerCalendarActivity', [$action, $data, $shares, $changedProperties]);
+ }
+
+ public function testUserDeletionDoesNotCreateActivity(): void {
+ $backend = $this->getBackend();
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->willReturn(false);
+
+ $this->activityManager->expects($this->never())
+ ->method('publish');
+
+ $this->invokePrivate($backend, 'triggerCalendarActivity', [Calendar::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], [], []]);
+ }
+
+ public static function dataGetUsersForShares(): array {
+ return [
+ [
+ [],
+ [],
+ [],
+ ],
+ [
+ [
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user1'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user3'],
+ ],
+ [],
+ ['user1', 'user2', 'user3'],
+ ],
+ [
+ [
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user1'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'],
+ ],
+ ['group2' => null, 'group3' => null],
+ ['user1', 'user2'],
+ ],
+ [
+ [
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user1'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'],
+ ],
+ ['group2' => ['user1', 'user2', 'user3'], 'group3' => ['user2', 'user3', 'user4']],
+ ['user1', 'user2', 'user3', 'user4'],
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGetUsersForShares')]
+ public function testGetUsersForShares(array $shares, array $groups, array $expected): void {
+ $backend = $this->getBackend();
+
+ $getGroups = [];
+ foreach ($groups as $gid => $members) {
+ if ($members === null) {
+ $getGroups[] = [$gid, null];
+ continue;
+ }
+
+ $group = $this->createMock(IGroup::class);
+ $group->expects($this->once())
+ ->method('getUsers')
+ ->willReturn($this->getUsers($members));
+
+ $getGroups[] = [$gid, $group];
+ }
+
+ $this->groupManager->expects($this->exactly(sizeof($getGroups)))
+ ->method('get')
+ ->willReturnMap($getGroups);
+
+ $users = $this->invokePrivate($backend, 'getUsersForShares', [$shares]);
+ sort($users);
+ $this->assertEquals($expected, $users);
+ }
+
+ /**
+ * @param string[] $users
+ * @return IUser[]&MockObject[]
+ */
+ protected function getUsers(array $users) {
+ $list = [];
+ foreach ($users as $user) {
+ $list[] = $this->getUserMock($user);
+ }
+ return $list;
+ }
+
+ /**
+ * @param string $uid
+ * @return IUser&MockObject
+ */
+ protected function getUserMock($uid) {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())
+ ->method('getUID')
+ ->willReturn($uid);
+ return $user;
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php b/apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php
new file mode 100644
index 00000000000..b4c4e14fe7d
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Activity\Filter;
+
+use OCA\DAV\CalDAV\Activity\Filter\Calendar;
+use OCP\Activity\IFilter;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class CalendarTest extends TestCase {
+ protected IURLGenerator&MockObject $url;
+ protected IFilter $filter;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->url = $this->createMock(IURLGenerator::class);
+ $l = $this->createMock(IL10N::class);
+ $l->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($string, $args) {
+ return vsprintf($string, $args);
+ });
+
+ $this->filter = new Calendar(
+ $l, $this->url
+ );
+ }
+
+ public function testGetIcon(): void {
+ $this->url->expects($this->once())
+ ->method('imagePath')
+ ->with('core', 'places/calendar.svg')
+ ->willReturn('path-to-icon');
+
+ $this->url->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('path-to-icon')
+ ->willReturn('absolute-path-to-icon');
+
+ $this->assertEquals('absolute-path-to-icon', $this->filter->getIcon());
+ }
+
+ public static function dataFilterTypes(): array {
+ return [
+ [[], []],
+ [['calendar', 'calendar_event'], ['calendar', 'calendar_event']],
+ [['calendar', 'calendar_event', 'calendar_todo'], ['calendar', 'calendar_event']],
+ [['calendar', 'calendar_event', 'files'], ['calendar', 'calendar_event']],
+ ];
+ }
+
+ /**
+ * @param string[] $types
+ * @param string[] $expected
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilterTypes')]
+ public function testFilterTypes(array $types, array $expected): void {
+ $this->assertEquals($expected, $this->filter->filterTypes($types));
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php b/apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php
new file mode 100644
index 00000000000..87b55f14bcc
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Activity\Filter;
+
+use OCA\DAV\CalDAV\Activity\Filter\Calendar;
+use OCA\DAV\CalDAV\Activity\Filter\Todo;
+use OCP\Activity\IFilter;
+use OCP\Server;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class GenericTest extends TestCase {
+ public static function dataFilters(): array {
+ return [
+ [Calendar::class],
+ [Todo::class],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testImplementsInterface(string $filterClass): void {
+ $filter = Server::get($filterClass);
+ $this->assertInstanceOf(IFilter::class, $filter);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testGetIdentifier(string $filterClass): void {
+ /** @var IFilter $filter */
+ $filter = Server::get($filterClass);
+ $this->assertIsString($filter->getIdentifier());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testGetName(string $filterClass): void {
+ /** @var IFilter $filter */
+ $filter = Server::get($filterClass);
+ $this->assertIsString($filter->getName());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testGetPriority(string $filterClass): void {
+ /** @var IFilter $filter */
+ $filter = Server::get($filterClass);
+ $priority = $filter->getPriority();
+ $this->assertIsInt($filter->getPriority());
+ $this->assertGreaterThanOrEqual(0, $priority);
+ $this->assertLessThanOrEqual(100, $priority);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testGetIcon(string $filterClass): void {
+ /** @var IFilter $filter */
+ $filter = Server::get($filterClass);
+ $this->assertIsString($filter->getIcon());
+ $this->assertStringStartsWith('http', $filter->getIcon());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testFilterTypes(string $filterClass): void {
+ /** @var IFilter $filter */
+ $filter = Server::get($filterClass);
+ $this->assertIsArray($filter->filterTypes([]));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testAllowedApps(string $filterClass): void {
+ /** @var IFilter $filter */
+ $filter = Server::get($filterClass);
+ $this->assertIsArray($filter->allowedApps());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php b/apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php
new file mode 100644
index 00000000000..f18d66b9774
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Activity\Filter;
+
+use OCA\DAV\CalDAV\Activity\Filter\Todo;
+use OCP\Activity\IFilter;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class TodoTest extends TestCase {
+ protected IURLGenerator&MockObject $url;
+ protected IFilter $filter;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->url = $this->createMock(IURLGenerator::class);
+ $l = $this->createMock(IL10N::class);
+ $l->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($string, $args) {
+ return vsprintf($string, $args);
+ });
+
+ $this->filter = new Todo(
+ $l, $this->url
+ );
+ }
+
+ public function testGetIcon(): void {
+ $this->url->expects($this->once())
+ ->method('imagePath')
+ ->with('core', 'actions/checkmark.svg')
+ ->willReturn('path-to-icon');
+
+ $this->url->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('path-to-icon')
+ ->willReturn('absolute-path-to-icon');
+
+ $this->assertEquals('absolute-path-to-icon', $this->filter->getIcon());
+ }
+
+ public static function dataFilterTypes(): array {
+ return [
+ [[], []],
+ [['calendar_todo'], ['calendar_todo']],
+ [['calendar', 'calendar_event', 'calendar_todo'], ['calendar_todo']],
+ [['calendar', 'calendar_todo', 'files'], ['calendar_todo']],
+ ];
+ }
+
+ /**
+ * @param string[] $types
+ * @param string[] $expected
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilterTypes')]
+ public function testFilterTypes(array $types, array $expected): void {
+ $this->assertEquals($expected, $this->filter->filterTypes($types));
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php b/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php
new file mode 100644
index 00000000000..3e6219beef8
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php
@@ -0,0 +1,116 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Activity\Provider;
+
+use OCA\DAV\CalDAV\Activity\Provider\Base;
+use OCP\Activity\IEvent;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class BaseTest extends TestCase {
+ protected IUserManager&MockObject $userManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IURLGenerator&MockObject $url;
+ protected Base&MockObject $provider;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->url = $this->createMock(IURLGenerator::class);
+ $this->provider = $this->getMockBuilder(Base::class)
+ ->setConstructorArgs([
+ $this->userManager,
+ $this->groupManager,
+ $this->url,
+ ])
+ ->onlyMethods(['parse'])
+ ->getMock();
+ }
+
+ public static function dataSetSubjects(): array {
+ return [
+ ['abc', []],
+ ['{actor} created {calendar}', ['actor' => ['name' => 'abc'], 'calendar' => ['name' => 'xyz']]],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSetSubjects')]
+ public function testSetSubjects(string $subject, array $parameters): void {
+ $event = $this->createMock(IEvent::class);
+ $event->expects($this->once())
+ ->method('setRichSubject')
+ ->with($subject, $parameters)
+ ->willReturnSelf();
+ $event->expects($this->never())
+ ->method('setParsedSubject');
+
+ $this->invokePrivate($this->provider, 'setSubjects', [$event, $subject, $parameters]);
+ }
+
+ public static function dataGenerateCalendarParameter(): array {
+ return [
+ [['id' => 23, 'uri' => 'foo', 'name' => 'bar'], 'bar'],
+ [['id' => 42, 'uri' => 'foo', 'name' => 'Personal'], 'Personal'],
+ [['id' => 42, 'uri' => 'personal', 'name' => 'bar'], 'bar'],
+ [['id' => 42, 'uri' => 'personal', 'name' => 'Personal'], 't(Personal)'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateCalendarParameter')]
+ public function testGenerateCalendarParameter(array $data, string $name): void {
+ $l = $this->createMock(IL10N::class);
+ $l->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($string, $args) {
+ return 't(' . vsprintf($string, $args) . ')';
+ });
+
+ $this->assertEquals([
+ 'type' => 'calendar',
+ 'id' => $data['id'],
+ 'name' => $name,
+ ], $this->invokePrivate($this->provider, 'generateCalendarParameter', [$data, $l]));
+ }
+
+ public static function dataGenerateLegacyCalendarParameter(): array {
+ return [
+ [23, 'c1'],
+ [42, 'c2'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateLegacyCalendarParameter')]
+ public function testGenerateLegacyCalendarParameter(int $id, string $name): void {
+ $this->assertEquals([
+ 'type' => 'calendar',
+ 'id' => $id,
+ 'name' => $name,
+ ], $this->invokePrivate($this->provider, 'generateLegacyCalendarParameter', [$id, $name]));
+ }
+
+ public static function dataGenerateGroupParameter(): array {
+ return [
+ ['g1'],
+ ['g2'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateGroupParameter')]
+ public function testGenerateGroupParameter(string $gid): void {
+ $this->assertEquals([
+ 'type' => 'user-group',
+ 'id' => $gid,
+ 'name' => $gid,
+ ], $this->invokePrivate($this->provider, 'generateGroupParameter', [$gid]));
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php b/apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php
new file mode 100644
index 00000000000..4fd38c1aed2
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php
@@ -0,0 +1,190 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Activity\Provider;
+
+use InvalidArgumentException;
+use OCA\DAV\CalDAV\Activity\Provider\Event;
+use OCP\Activity\IEventMerger;
+use OCP\Activity\IManager;
+use OCP\App\IAppManager;
+use OCP\IGroupManager;
+use OCP\IURLGenerator;
+use OCP\IUserManager;
+use OCP\L10N\IFactory;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+use TypeError;
+
+class EventTest extends TestCase {
+ protected IUserManager&MockObject $userManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IURLGenerator&MockObject $url;
+ protected IAppManager&MockObject $appManager;
+ protected IFactory&MockObject $i10nFactory;
+ protected IManager&MockObject $activityManager;
+ protected IEventMerger&MockObject $eventMerger;
+ protected Event&MockObject $provider;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->i10nFactory = $this->createMock(IFactory::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->activityManager = $this->createMock(IManager::class);
+ $this->url = $this->createMock(IURLGenerator::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->eventMerger = $this->createMock(IEventMerger::class);
+ $this->provider = $this->getMockBuilder(Event::class)
+ ->setConstructorArgs([
+ $this->i10nFactory,
+ $this->url,
+ $this->activityManager,
+ $this->userManager,
+ $this->groupManager,
+ $this->eventMerger,
+ $this->appManager
+ ])
+ ->onlyMethods(['parse'])
+ ->getMock();
+ }
+
+ public static function dataGenerateObjectParameter(): array {
+ $link = [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'personal',
+ 'owner' => 'someuser'
+ ];
+
+ return [
+ [23, 'c1', $link, true],
+ [23, 'c1', $link, false],
+ [42, 'c2', null],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateObjectParameter')]
+ public function testGenerateObjectParameter(int $id, string $name, ?array $link, bool $calendarAppEnabled = true): void {
+ $affectedUser = 'otheruser';
+ if ($link) {
+ $affectedUser = $link['owner'];
+ $generatedLink = [
+ 'objectId' => base64_encode('/remote.php/dav/calendars/' . $link['owner'] . '/' . $link['calendar_uri'] . '/' . $link['object_uri']),
+ ];
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('calendar')
+ ->willReturn($calendarAppEnabled);
+ if ($calendarAppEnabled) {
+ $this->url->expects($this->once())
+ ->method('getWebroot');
+ $this->url->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with('calendar.view.indexdirect.edit', $generatedLink)
+ ->willReturn('fullLink');
+ }
+ }
+ $objectParameter = ['id' => $id, 'name' => $name];
+ if ($link) {
+ $objectParameter['link'] = $link;
+ }
+ $result = [
+ 'type' => 'calendar-event',
+ 'id' => $id,
+ 'name' => $name,
+ ];
+ if ($link && $calendarAppEnabled) {
+ $result['link'] = 'fullLink';
+ }
+ $this->assertEquals($result, $this->invokePrivate($this->provider, 'generateObjectParameter', [$objectParameter, $affectedUser]));
+ }
+
+ public static function generateObjectParameterLinkEncodingDataProvider(): array {
+ return [
+ [ // Shared calendar
+ [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'personal',
+ 'owner' => 'sharer'
+ ],
+ base64_encode('/remote.php/dav/calendars/sharee/personal_shared_by_sharer/someuuid.ics'),
+ ],
+ [ // Shared calendar with umlauts
+ [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'umlaut_äüöß',
+ 'owner' => 'sharer'
+ ],
+ base64_encode('/remote.php/dav/calendars/sharee/umlaut_%c3%a4%c3%bc%c3%b6%c3%9f_shared_by_sharer/someuuid.ics'),
+ ],
+ [ // Shared calendar with umlauts and mixed casing
+ [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'Umlaut_äüöß',
+ 'owner' => 'sharer'
+ ],
+ base64_encode('/remote.php/dav/calendars/sharee/Umlaut_%c3%a4%c3%bc%c3%b6%c3%9f_shared_by_sharer/someuuid.ics'),
+ ],
+ [ // Owned calendar with umlauts
+ [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'umlaut_äüöß',
+ 'owner' => 'sharee'
+ ],
+ base64_encode('/remote.php/dav/calendars/sharee/umlaut_%c3%a4%c3%bc%c3%b6%c3%9f/someuuid.ics'),
+ ],
+ [ // Owned calendar with umlauts and mixed casing
+ [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'Umlaut_äüöß',
+ 'owner' => 'sharee'
+ ],
+ base64_encode('/remote.php/dav/calendars/sharee/Umlaut_%c3%a4%c3%bc%c3%b6%c3%9f/someuuid.ics'),
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('generateObjectParameterLinkEncodingDataProvider')]
+ public function testGenerateObjectParameterLinkEncoding(array $link, string $objectId): void {
+ $generatedLink = [
+ 'objectId' => $objectId,
+ ];
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('calendar')
+ ->willReturn(true);
+ $this->url->expects($this->once())
+ ->method('getWebroot');
+ $this->url->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with('calendar.view.indexdirect.edit', $generatedLink)
+ ->willReturn('fullLink');
+ $objectParameter = ['id' => 42, 'name' => 'calendar', 'link' => $link];
+ $result = [
+ 'type' => 'calendar-event',
+ 'id' => 42,
+ 'name' => 'calendar',
+ 'link' => 'fullLink',
+ ];
+ $this->assertEquals($result, $this->invokePrivate($this->provider, 'generateObjectParameter', [$objectParameter, 'sharee']));
+ }
+
+ public static function dataGenerateObjectParameterThrows(): array {
+ return [
+ ['event', TypeError::class],
+ [['name' => 'event']],
+ [['id' => 42]],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateObjectParameterThrows')]
+ public function testGenerateObjectParameterThrows(string|array $eventData, string $exception = InvalidArgumentException::class): void {
+ $this->expectException($exception);
+
+ $this->invokePrivate($this->provider, 'generateObjectParameter', [$eventData, 'no_user']);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php b/apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php
new file mode 100644
index 00000000000..23126b6bbcf
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php
@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Activity\Setting;
+
+use OCA\DAV\CalDAV\Activity\Setting\Calendar;
+use OCA\DAV\CalDAV\Activity\Setting\Event;
+use OCA\DAV\CalDAV\Activity\Setting\Todo;
+use OCP\Activity\ISetting;
+use OCP\Server;
+use Test\TestCase;
+
+class GenericTest extends TestCase {
+ public static function dataSettings(): array {
+ return [
+ [Calendar::class],
+ [Event::class],
+ [Todo::class],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testImplementsInterface(string $settingClass): void {
+ $setting = Server::get($settingClass);
+ $this->assertInstanceOf(ISetting::class, $setting);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testGetIdentifier(string $settingClass): void {
+ /** @var ISetting $setting */
+ $setting = Server::get($settingClass);
+ $this->assertIsString($setting->getIdentifier());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testGetName(string $settingClass): void {
+ /** @var ISetting $setting */
+ $setting = Server::get($settingClass);
+ $this->assertIsString($setting->getName());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testGetPriority(string $settingClass): void {
+ /** @var ISetting $setting */
+ $setting = Server::get($settingClass);
+ $priority = $setting->getPriority();
+ $this->assertIsInt($setting->getPriority());
+ $this->assertGreaterThanOrEqual(0, $priority);
+ $this->assertLessThanOrEqual(100, $priority);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testCanChangeStream(string $settingClass): void {
+ /** @var ISetting $setting */
+ $setting = Server::get($settingClass);
+ $this->assertIsBool($setting->canChangeStream());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testIsDefaultEnabledStream(string $settingClass): void {
+ /** @var ISetting $setting */
+ $setting = Server::get($settingClass);
+ $this->assertIsBool($setting->isDefaultEnabledStream());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testCanChangeMail(string $settingClass): void {
+ /** @var ISetting $setting */
+ $setting = Server::get($settingClass);
+ $this->assertIsBool($setting->canChangeMail());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testIsDefaultEnabledMail(string $settingClass): void {
+ /** @var ISetting $setting */
+ $setting = Server::get($settingClass);
+ $this->assertIsBool($setting->isDefaultEnabledMail());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php b/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php
new file mode 100644
index 00000000000..84879e87238
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php
@@ -0,0 +1,120 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\AppCalendar;
+
+use OCA\DAV\CalDAV\AppCalendar\AppCalendar;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICreateFromString;
+use OCP\Constants;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+use function rewind;
+
+class AppCalendarTest extends TestCase {
+ private string $principal = 'principals/users/foo';
+
+ private AppCalendar $appCalendar;
+ private AppCalendar $writeableAppCalendar;
+
+ private ICalendar&MockObject $calendar;
+ private ICalendar&MockObject $writeableCalendar;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calendar = $this->getMockBuilder(ICalendar::class)->getMock();
+ $this->calendar->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_READ);
+
+ $this->writeableCalendar = $this->getMockBuilder(ICreateFromString::class)->getMock();
+ $this->writeableCalendar->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE);
+
+ $this->appCalendar = new AppCalendar('dav-wrapper', $this->calendar, $this->principal);
+ $this->writeableAppCalendar = new AppCalendar('dav-wrapper', $this->writeableCalendar, $this->principal);
+ }
+
+ public function testGetPrincipal():void {
+ // Check that the correct name is returned
+ $this->assertEquals($this->principal, $this->appCalendar->getOwner());
+ $this->assertEquals($this->principal, $this->writeableAppCalendar->getOwner());
+ }
+
+ public function testDelete(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('Deleting an entry is not implemented');
+
+ $this->appCalendar->delete();
+ }
+
+ public function testCreateFile(): void {
+ $calls = [
+ ['some-name', 'data'],
+ ['other-name', ''],
+ ['name', 'some data'],
+ ];
+ $this->writeableCalendar->expects($this->exactly(3))
+ ->method('createFromString')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ // pass data
+ $this->assertNull($this->writeableAppCalendar->createFile('some-name', 'data'));
+ // null is empty string
+ $this->assertNull($this->writeableAppCalendar->createFile('other-name', null));
+ // resource to data
+ $fp = fopen('php://memory', 'r+');
+ fwrite($fp, 'some data');
+ rewind($fp);
+ $this->assertNull($this->writeableAppCalendar->createFile('name', $fp));
+ fclose($fp);
+ }
+
+ public function testCreateFile_readOnly(): void {
+ // If writing is not supported
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('Creating a new entry is not allowed');
+
+ $this->appCalendar->createFile('some-name', 'data');
+ }
+
+ public function testSetACL(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('Setting ACL is not supported on this node');
+
+ $this->appCalendar->setACL([]);
+ }
+
+ public function testGetACL():void {
+ $expectedRO = [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principal,
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->principal,
+ 'protected' => true,
+ ]
+ ];
+ $expectedRW = $expectedRO;
+ $expectedRW[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principal,
+ 'protected' => true,
+ ];
+
+ // Check that the correct ACL is returned (default be only readable)
+ $this->assertEquals($expectedRO, $this->appCalendar->getACL());
+ $this->assertEquals($expectedRW, $this->writeableAppCalendar->getACL());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php b/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php
new file mode 100644
index 00000000000..3d72d5c97b8
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php
@@ -0,0 +1,170 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\AppCalendar;
+
+use OCA\DAV\CalDAV\AppCalendar\AppCalendar;
+use OCA\DAV\CalDAV\AppCalendar\CalendarObject;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICreateFromString;
+use OCP\Constants;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+class CalendarObjectTest extends TestCase {
+ private CalendarObject $calendarObject;
+ private AppCalendar&MockObject $calendar;
+ private ICalendar&MockObject $backend;
+ private VCalendar&MockObject $vobject;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calendar = $this->createMock(AppCalendar::class);
+ $this->calendar->method('getOwner')->willReturn('owner');
+ $this->calendar->method('getGroup')->willReturn('group');
+
+ $this->backend = $this->createMock(ICalendar::class);
+ $this->vobject = $this->createMock(VCalendar::class);
+ $this->calendarObject = new CalendarObject($this->calendar, $this->backend, $this->vobject);
+ }
+
+ public function testGetOwner(): void {
+ $this->assertEquals($this->calendarObject->getOwner(), 'owner');
+ }
+
+ public function testGetGroup(): void {
+ $this->assertEquals($this->calendarObject->getGroup(), 'group');
+ }
+
+ public function testGetACL(): void {
+ $this->calendar->expects($this->exactly(2))
+ ->method('getPermissions')
+ ->willReturnOnConsecutiveCalls(Constants::PERMISSION_READ, Constants::PERMISSION_ALL);
+
+ // read only
+ $this->assertEquals($this->calendarObject->getACL(), [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'owner',
+ 'protected' => true,
+ ]
+ ]);
+
+ // write permissions
+ $this->assertEquals($this->calendarObject->getACL(), [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'owner',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write-content',
+ 'principal' => 'owner',
+ 'protected' => true,
+ ]
+ ]);
+ }
+
+ public function testSetACL(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->calendarObject->setACL([]);
+ }
+
+ public function testPut_readOnlyBackend(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->calendarObject->put('foo');
+ }
+
+ public function testPut_noPermissions(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $backend = $this->createMock(ICreateFromString::class);
+ $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject);
+
+ $this->calendar->expects($this->once())
+ ->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_READ);
+
+ $calendarObject->put('foo');
+ }
+
+ public function testPut(): void {
+ $backend = $this->createMock(ICreateFromString::class);
+ $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject);
+
+ $this->vobject->expects($this->once())
+ ->method('getBaseComponent')
+ ->willReturn((object)['UID' => 'someid']);
+ $this->calendar->expects($this->once())
+ ->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_ALL);
+
+ $backend->expects($this->once())
+ ->method('createFromString')
+ ->with('someid.ics', 'foo');
+ $calendarObject->put('foo');
+ }
+
+ public function testGet(): void {
+ $this->vobject->expects($this->once())
+ ->method('serialize')
+ ->willReturn('foo');
+ $this->assertEquals($this->calendarObject->get(), 'foo');
+ }
+
+ public function testDelete_notWriteable(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->calendarObject->delete();
+ }
+
+ public function testDelete_noPermission(): void {
+ $backend = $this->createMock(ICreateFromString::class);
+ $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject);
+
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $calendarObject->delete();
+ }
+
+ public function testDelete(): void {
+ $backend = $this->createMock(ICreateFromString::class);
+ $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject);
+
+ $components = [(new VCalendar(['VEVENT' => ['UID' => 'someid']]))->getBaseComponent()];
+
+ $this->calendar->expects($this->once())
+ ->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_DELETE);
+ $this->vobject->expects($this->once())
+ ->method('getBaseComponents')
+ ->willReturn($components);
+ $this->vobject->expects($this->once())
+ ->method('getBaseComponent')
+ ->willReturn($components[0]);
+
+ $backend->expects($this->once())
+ ->method('createFromString')
+ ->with('someid.ics', self::callback(fn ($data): bool => preg_match('/BEGIN:VEVENT(.|\r\n)+STATUS:CANCELLED/', $data) === 1));
+
+ $calendarObject->delete();
+ }
+
+ public function testGetName(): void {
+ $this->vobject->expects($this->exactly(2))
+ ->method('getBaseComponent')
+ ->willReturnOnConsecutiveCalls((object)['UID' => 'someid'], (object)['UID' => 'someid', 'X-FILENAME' => 'real-filename.ics']);
+
+ $this->assertEquals($this->calendarObject->getName(), 'someid.ics');
+ $this->assertEquals($this->calendarObject->getName(), 'real-filename.ics');
+ }
+
+ public function testSetName(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->calendarObject->setName('Some name');
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php b/apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php
new file mode 100644
index 00000000000..a5811271ce2
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php
@@ -0,0 +1,221 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\BirthdayCalendar;
+
+use OCA\DAV\CalDAV\BirthdayCalendar\EnablePlugin;
+use OCA\DAV\CalDAV\BirthdayService;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\CalendarHome;
+use OCP\IConfig;
+use OCP\IUser;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class EnablePluginTest extends TestCase {
+ protected \Sabre\DAV\Server&MockObject $server;
+ protected IConfig&MockObject $config;
+ protected BirthdayService&MockObject $birthdayService;
+ protected IUser&MockObject $user;
+ protected EnablePlugin $plugin;
+
+ protected $request;
+
+ protected $response;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->server = $this->createMock(\Sabre\DAV\Server::class);
+ $this->server->tree = $this->createMock(\Sabre\DAV\Tree::class);
+ $this->server->httpResponse = $this->createMock(\Sabre\HTTP\Response::class);
+ $this->server->xml = $this->createMock(\Sabre\DAV\Xml\Service::class);
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->birthdayService = $this->createMock(BirthdayService::class);
+ $this->user = $this->createMock(IUser::class);
+
+ $this->plugin = new EnablePlugin($this->config, $this->birthdayService, $this->user);
+ $this->plugin->initialize($this->server);
+
+ $this->request = $this->createMock(\Sabre\HTTP\RequestInterface::class);
+ $this->response = $this->createMock(\Sabre\HTTP\ResponseInterface::class);
+ }
+
+ public function testGetFeatures(): void {
+ $this->assertEquals(['nc-enable-birthday-calendar'], $this->plugin->getFeatures());
+ }
+
+ public function testGetName(): void {
+ $this->assertEquals('nc-enable-birthday-calendar', $this->plugin->getPluginName());
+ }
+
+ public function testInitialize(): void {
+ $server = $this->createMock(\Sabre\DAV\Server::class);
+
+ $plugin = new EnablePlugin($this->config, $this->birthdayService, $this->user);
+
+ $server->expects($this->once())
+ ->method('on')
+ ->with('method:POST', [$plugin, 'httpPost']);
+
+ $plugin->initialize($server);
+ }
+
+ public function testHttpPostNoCalendarHome(): void {
+ $calendar = $this->createMock(Calendar::class);
+
+ $this->server->expects($this->once())
+ ->method('getRequestUri')
+ ->willReturn('/bar/foo');
+ $this->server->tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('/bar/foo')
+ ->willReturn($calendar);
+
+ $this->config->expects($this->never())
+ ->method('setUserValue');
+
+ $this->birthdayService->expects($this->never())
+ ->method('syncUser');
+
+ $this->plugin->httpPost($this->request, $this->response);
+ }
+
+ public function testHttpPostWrongRequest(): void {
+ $calendarHome = $this->createMock(CalendarHome::class);
+
+ $this->server->expects($this->once())
+ ->method('getRequestUri')
+ ->willReturn('/bar/foo');
+ $this->server->tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('/bar/foo')
+ ->willReturn($calendarHome);
+
+ $this->request->expects($this->once())
+ ->method('getBodyAsString')
+ ->willReturn('<nc:disable-birthday-calendar xmlns:nc="http://nextcloud.com/ns"/>');
+
+ $this->request->expects($this->once())
+ ->method('getUrl')
+ ->willReturn('url_abc');
+
+ $this->server->xml->expects($this->once())
+ ->method('parse')
+ ->willReturnCallback(function ($requestBody, $url, &$documentType): void {
+ $documentType = '{http://nextcloud.com/ns}disable-birthday-calendar';
+ });
+
+ $this->config->expects($this->never())
+ ->method('setUserValue');
+
+ $this->birthdayService->expects($this->never())
+ ->method('syncUser');
+
+ $this->plugin->httpPost($this->request, $this->response);
+ }
+
+ public function testHttpPostNotAuthorized(): void {
+ $calendarHome = $this->createMock(CalendarHome::class);
+
+ $this->server->expects($this->once())
+ ->method('getRequestUri')
+ ->willReturn('/bar/foo');
+ $this->server->tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('/bar/foo')
+ ->willReturn($calendarHome);
+
+ $calendarHome->expects($this->once())
+ ->method('getOwner')
+ ->willReturn('principals/users/BlaBlub');
+
+ $this->request->expects($this->once())
+ ->method('getBodyAsString')
+ ->willReturn('<nc:enable-birthday-calendar xmlns:nc="http://nextcloud.com/ns"/>');
+
+ $this->request->expects($this->once())
+ ->method('getUrl')
+ ->willReturn('url_abc');
+
+ $this->server->xml->expects($this->once())
+ ->method('parse')
+ ->willReturnCallback(function ($requestBody, $url, &$documentType): void {
+ $documentType = '{http://nextcloud.com/ns}enable-birthday-calendar';
+ });
+
+ $this->user->expects(self::once())
+ ->method('getUID')
+ ->willReturn('admin');
+
+ $this->server->httpResponse->expects($this->once())
+ ->method('setStatus')
+ ->with(403);
+
+ $this->config->expects($this->never())
+ ->method('setUserValue');
+
+ $this->birthdayService->expects($this->never())
+ ->method('syncUser');
+
+
+ $result = $this->plugin->httpPost($this->request, $this->response);
+
+ $this->assertEquals(false, $result);
+ }
+
+ public function testHttpPost(): void {
+ $calendarHome = $this->createMock(CalendarHome::class);
+
+ $this->server->expects($this->once())
+ ->method('getRequestUri')
+ ->willReturn('/bar/foo');
+ $this->server->tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('/bar/foo')
+ ->willReturn($calendarHome);
+
+ $calendarHome->expects($this->once())
+ ->method('getOwner')
+ ->willReturn('principals/users/BlaBlub');
+
+ $this->request->expects($this->once())
+ ->method('getBodyAsString')
+ ->willReturn('<nc:enable-birthday-calendar xmlns:nc="http://nextcloud.com/ns"/>');
+
+ $this->request->expects($this->once())
+ ->method('getUrl')
+ ->willReturn('url_abc');
+
+ $this->server->xml->expects($this->once())
+ ->method('parse')
+ ->willReturnCallback(function ($requestBody, $url, &$documentType): void {
+ $documentType = '{http://nextcloud.com/ns}enable-birthday-calendar';
+ });
+
+ $this->user->expects(self::exactly(3))
+ ->method('getUID')
+ ->willReturn('BlaBlub');
+
+ $this->config->expects($this->once())
+ ->method('setUserValue')
+ ->with('BlaBlub', 'dav', 'generateBirthdayCalendar', 'yes');
+
+ $this->birthdayService->expects($this->once())
+ ->method('syncUser')
+ ->with('BlaBlub');
+
+ $this->server->httpResponse->expects($this->once())
+ ->method('setStatus')
+ ->with(204);
+
+ $result = $this->plugin->httpPost($this->request, $this->response);
+
+ $this->assertEquals(false, $result);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php
new file mode 100644
index 00000000000..935d8314f29
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\CachedSubscription;
+use OCA\DAV\CalDAV\CachedSubscriptionImpl;
+use OCA\DAV\CalDAV\CalDavBackend;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class CachedSubscriptionImplTest extends TestCase {
+ private CachedSubscription&MockObject $cachedSubscription;
+ private array $cachedSubscriptionInfo;
+ private CalDavBackend&MockObject $backend;
+ private CachedSubscriptionImpl $cachedSubscriptionImpl;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->cachedSubscription = $this->createMock(CachedSubscription::class);
+ $this->cachedSubscriptionInfo = [
+ 'id' => 'fancy_id_123',
+ '{DAV:}displayname' => 'user readable name 123',
+ '{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
+ 'uri' => '/this/is/a/uri',
+ 'source' => 'https://test.localhost/calendar1',
+ ];
+ $this->backend = $this->createMock(CalDavBackend::class);
+
+ $this->cachedSubscriptionImpl = new CachedSubscriptionImpl(
+ $this->cachedSubscription,
+ $this->cachedSubscriptionInfo,
+ $this->backend
+ );
+ }
+
+ public function testGetKey(): void {
+ $this->assertEquals($this->cachedSubscriptionImpl->getKey(), 'fancy_id_123');
+ }
+
+ public function testGetDisplayname(): void {
+ $this->assertEquals($this->cachedSubscriptionImpl->getDisplayName(), 'user readable name 123');
+ }
+
+ public function testGetDisplayColor(): void {
+ $this->assertEquals($this->cachedSubscriptionImpl->getDisplayColor(), '#AABBCC');
+ }
+
+ public function testGetSource(): void {
+ $this->assertEquals($this->cachedSubscriptionImpl->getSource(), 'https://test.localhost/calendar1');
+ }
+
+ public function testSearch(): void {
+ $this->backend->expects($this->once())
+ ->method('search')
+ ->with($this->cachedSubscriptionInfo, 'abc', ['def'], ['ghi'], 42, 1337)
+ ->willReturn(['SEARCHRESULTS']);
+
+ $result = $this->cachedSubscriptionImpl->search('abc', ['def'], ['ghi'], 42, 1337);
+ $this->assertEquals($result, ['SEARCHRESULTS']);
+ }
+
+ public function testGetPermissionRead(): void {
+ $this->cachedSubscription->expects($this->once())
+ ->method('getACL')
+ ->with()
+ ->willReturn([
+ ['privilege' => '{DAV:}read']
+ ]);
+
+ $this->assertEquals(1, $this->cachedSubscriptionImpl->getPermissions());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php
new file mode 100644
index 00000000000..03a2c9f20ee
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php
@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\CachedSubscriptionObject;
+use OCA\DAV\CalDAV\CalDavBackend;
+
+class CachedSubscriptionObjectTest extends \Test\TestCase {
+ public function testGet(): void {
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ $objectData = [
+ 'uri' => 'foo123'
+ ];
+
+ $backend->expects($this->once())
+ ->method('getCalendarObject')
+ ->with(666, 'foo123', 1)
+ ->willReturn([
+ 'calendardata' => 'BEGIN...',
+ ]);
+
+ $calendarObject = new CachedSubscriptionObject($backend, $calendarInfo, $objectData);
+ $this->assertEquals('BEGIN...', $calendarObject->get());
+ }
+
+
+ public function testPut(): void {
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+ $this->expectExceptionMessage('Creating objects in a cached subscription is not allowed');
+
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ $objectData = [
+ 'uri' => 'foo123'
+ ];
+
+ $calendarObject = new CachedSubscriptionObject($backend, $calendarInfo, $objectData);
+ $calendarObject->put('');
+ }
+
+
+ public function testDelete(): void {
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+ $this->expectExceptionMessage('Deleting objects in a cached subscription is not allowed');
+
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ $objectData = [
+ 'uri' => 'foo123'
+ ];
+
+ $calendarObject = new CachedSubscriptionObject($backend, $calendarInfo, $objectData);
+ $calendarObject->delete();
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php
new file mode 100644
index 00000000000..58d5ca7835c
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\CachedSubscriptionImpl;
+use OCA\DAV\CalDAV\CachedSubscriptionProvider;
+use OCA\DAV\CalDAV\CalDavBackend;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class CachedSubscriptionProviderTest extends TestCase {
+
+ private CalDavBackend&MockObject $backend;
+ private CachedSubscriptionProvider $provider;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->backend = $this->createMock(CalDavBackend::class);
+ $this->backend
+ ->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('user-principal-123')
+ ->willReturn([
+ [
+ 'id' => 'subscription-1',
+ 'uri' => 'subscription-1',
+ 'principaluris' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-1',
+ // A subscription array has actually more properties.
+ ],
+ [
+ 'id' => 'subscription-2',
+ 'uri' => 'subscription-2',
+ 'principaluri' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-2',
+ // A subscription array has actually more properties.
+ ]
+ ]);
+
+ $this->provider = new CachedSubscriptionProvider($this->backend);
+ }
+
+ public function testGetCalendars(): void {
+ $calendars = $this->provider->getCalendars(
+ 'user-principal-123',
+ []
+ );
+
+ $this->assertCount(2, $calendars);
+ $this->assertInstanceOf(CachedSubscriptionImpl::class, $calendars[0]);
+ $this->assertInstanceOf(CachedSubscriptionImpl::class, $calendars[1]);
+ }
+
+ public function testGetCalendarsFilterByUri(): void {
+ $calendars = $this->provider->getCalendars(
+ 'user-principal-123',
+ ['subscription-1']
+ );
+
+ $this->assertCount(1, $calendars);
+ $this->assertInstanceOf(CachedSubscriptionImpl::class, $calendars[0]);
+ $this->assertEquals('subscription-1', $calendars[0]->getUri());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php
new file mode 100644
index 00000000000..ba0da422290
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php
@@ -0,0 +1,296 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\CachedSubscription;
+use OCA\DAV\CalDAV\CachedSubscriptionObject;
+use OCA\DAV\CalDAV\CalDavBackend;
+use Sabre\DAV\PropPatch;
+
+class CachedSubscriptionTest extends \Test\TestCase {
+ public function testGetACL(): void {
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+ $this->assertEquals([
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user1',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user1/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user1/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{urn:ietf:params:xml:ns:caldav}read-free-busy',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => 'user1',
+ 'protected' => 'true'
+ ]
+ ], $calendar->getACL());
+ }
+
+ public function testGetChildACL(): void {
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+ $this->assertEquals([
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user1',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user1/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user1/calendar-proxy-read',
+ 'protected' => true,
+ ]
+ ], $calendar->getChildACL());
+ }
+
+ public function testGetOwner(): void {
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+ $this->assertEquals('user1', $calendar->getOwner());
+ }
+
+ public function testDelete(): void {
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $backend->expects($this->once())
+ ->method('deleteSubscription')
+ ->with(666);
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+ $calendar->delete();
+ }
+
+ public function testPropPatch(): void {
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ $propPatch = $this->createMock(PropPatch::class);
+
+ $backend->expects($this->once())
+ ->method('updateSubscription')
+ ->with(666, $propPatch);
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+ $calendar->propPatch($propPatch);
+ }
+
+
+ public function testGetChild(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ $this->expectExceptionMessage('Calendar object not found');
+
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $calls = [
+ [666, 'foo1', 1, [
+ 'id' => 99,
+ 'uri' => 'foo1'
+ ]],
+ [666, 'foo2', 1, null],
+ ];
+ $backend->expects($this->exactly(2))
+ ->method('getCalendarObject')
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $return = array_pop($expected);
+ $this->assertEquals($expected, func_get_args());
+ return $return;
+ });
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+
+ $first = $calendar->getChild('foo1');
+ $this->assertInstanceOf(CachedSubscriptionObject::class, $first);
+
+ $calendar->getChild('foo2');
+ }
+
+ public function testGetChildren(): void {
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $backend->expects($this->once())
+ ->method('getCalendarObjects')
+ ->with(666, 1)
+ ->willReturn([
+ [
+ 'id' => 99,
+ 'uri' => 'foo1'
+ ],
+ [
+ 'id' => 100,
+ 'uri' => 'foo2'
+ ],
+ ]);
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+
+ $res = $calendar->getChildren();
+ $this->assertCount(2, $res);
+ $this->assertInstanceOf(CachedSubscriptionObject::class, $res[0]);
+ $this->assertInstanceOf(CachedSubscriptionObject::class, $res[1]);
+ }
+
+ public function testGetMultipleChildren(): void {
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $backend->expects($this->once())
+ ->method('getMultipleCalendarObjects')
+ ->with(666, ['foo1', 'foo2'], 1)
+ ->willReturn([
+ [
+ 'id' => 99,
+ 'uri' => 'foo1'
+ ],
+ [
+ 'id' => 100,
+ 'uri' => 'foo2'
+ ],
+ ]);
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+
+ $res = $calendar->getMultipleChildren(['foo1', 'foo2']);
+ $this->assertCount(2, $res);
+ $this->assertInstanceOf(CachedSubscriptionObject::class, $res[0]);
+ $this->assertInstanceOf(CachedSubscriptionObject::class, $res[1]);
+ }
+
+
+ public function testCreateFile(): void {
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+ $this->expectExceptionMessage('Creating objects in cached subscription is not allowed');
+
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+ $calendar->createFile('foo', []);
+ }
+
+ public function testChildExists(): void {
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $calls = [
+ [666, 'foo1', 1, [
+ 'id' => 99,
+ 'uri' => 'foo1'
+ ]],
+ [666, 'foo2', 1, null],
+ ];
+ $backend->expects($this->exactly(2))
+ ->method('getCalendarObject')
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $return = array_pop($expected);
+ $this->assertEquals($expected, func_get_args());
+ return $return;
+ });
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+
+ $this->assertEquals(true, $calendar->childExists('foo1'));
+ $this->assertEquals(false, $calendar->childExists('foo2'));
+ }
+
+ public function testCalendarQuery(): void {
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $backend->expects($this->once())
+ ->method('calendarQuery')
+ ->with(666, ['foo'], 1)
+ ->willReturn([99]);
+
+ $calendar = new CachedSubscription($backend, $calendarInfo);
+
+ $this->assertEquals([99], $calendar->calendarQuery(['foo']));
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php
new file mode 100644
index 00000000000..f9205d5d322
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php
@@ -0,0 +1,1885 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use DateInterval;
+use DateTime;
+use DateTimeImmutable;
+use DateTimeZone;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\DAV\Sharing\Plugin as SharingPlugin;
+use OCA\DAV\Events\CalendarDeletedEvent;
+use OCP\IConfig;
+use OCP\IL10N;
+use Psr\Log\NullLogger;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\PropPatch;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\DAVACL\IACL;
+use function time;
+
+/**
+ * Class CalDavBackendTest
+ *
+ * @group DB
+ */
+class CalDavBackendTest extends AbstractCalDavBackend {
+ public function testCalendarOperations(): void {
+ $calendarId = $this->createTestCalendar();
+
+ // update its display name
+ $patch = new PropPatch([
+ '{DAV:}displayname' => 'Unit test',
+ '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar used for unit testing'
+ ]);
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->updateCalendar($calendarId, $patch);
+ $patch->commit();
+ $this->assertEquals(1, $this->backend->getCalendarsForUserCount(self::UNIT_TEST_USER));
+ $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+ $this->assertCount(1, $calendars);
+ $this->assertEquals('Unit test', $calendars[0]['{DAV:}displayname']);
+ $this->assertEquals('Calendar used for unit testing', $calendars[0]['{urn:ietf:params:xml:ns:caldav}calendar-description']);
+ $this->assertEquals('User\'s displayname', $calendars[0]['{http://nextcloud.com/ns}owner-displayname']);
+
+ // delete the address book
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->deleteCalendar($calendars[0]['id'], true);
+ $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+ self::assertEmpty($calendars);
+ }
+
+ public static function providesSharingData(): array {
+ return [
+ [true, true, true, false, [
+ [
+ 'href' => 'principal:' . self::UNIT_TEST_USER1,
+ 'readOnly' => false
+ ],
+ [
+ 'href' => 'principal:' . self::UNIT_TEST_GROUP,
+ 'readOnly' => true
+ ]
+ ], [
+ self::UNIT_TEST_USER1,
+ self::UNIT_TEST_GROUP,
+ ]],
+ [true, true, true, false, [
+ [
+ 'href' => 'principal:' . self::UNIT_TEST_GROUP,
+ 'readOnly' => true,
+ ],
+ [
+ 'href' => 'principal:' . self::UNIT_TEST_GROUP2,
+ 'readOnly' => false,
+ ],
+ ], [
+ self::UNIT_TEST_GROUP,
+ self::UNIT_TEST_GROUP2,
+ ]],
+ [true, true, true, true, [
+ [
+ 'href' => 'principal:' . self::UNIT_TEST_GROUP,
+ 'readOnly' => false,
+ ],
+ [
+ 'href' => 'principal:' . self::UNIT_TEST_GROUP2,
+ 'readOnly' => true,
+ ],
+ ], [
+ self::UNIT_TEST_GROUP,
+ self::UNIT_TEST_GROUP2,
+ ]],
+ [true, false, false, false, [
+ [
+ 'href' => 'principal:' . self::UNIT_TEST_USER1,
+ 'readOnly' => true
+ ],
+ ], [
+ self::UNIT_TEST_USER1,
+ ]],
+
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesSharingData')]
+ public function testCalendarSharing($userCanRead, $userCanWrite, $groupCanRead, $groupCanWrite, $add, $principals): void {
+ $logger = $this->createMock(\Psr\Log\LoggerInterface::class);
+ $config = $this->createMock(IConfig::class);
+
+ $l10n = $this->createMock(IL10N::class);
+ $l10n->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($text, $parameters = []) {
+ return vsprintf($text, $parameters);
+ });
+
+ $this->userManager->expects($this->any())
+ ->method('userExists')
+ ->willReturn(true);
+ $this->groupManager->expects($this->any())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->principal->expects(self::atLeastOnce())
+ ->method('findByUri')
+ ->willReturnOnConsecutiveCalls(...$principals);
+
+ $calendarId = $this->createTestCalendar();
+ $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+ $this->assertCount(1, $calendars);
+ $calendar = new Calendar($this->backend, $calendars[0], $l10n, $config, $logger);
+ $this->backend->updateShares($calendar, $add, []);
+ $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER1);
+ $this->assertCount(1, $calendars);
+ $calendar = new Calendar($this->backend, $calendars[0], $l10n, $config, $logger);
+ $acl = $calendar->getACL();
+ $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}read', $acl);
+ $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}write', $acl);
+ $this->assertAccess($userCanRead, self::UNIT_TEST_USER1, '{DAV:}read', $acl);
+ $this->assertAccess($userCanWrite, self::UNIT_TEST_USER1, '{DAV:}write', $acl);
+ $this->assertEquals(self::UNIT_TEST_USER, $calendar->getOwner());
+
+ // test acls on the child
+ $uri = static::getUniqueID('calobj');
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+ /** @var IACL $child */
+ $child = $calendar->getChild($uri);
+ $acl = $child->getACL();
+ $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}read', $acl);
+ $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}write', $acl);
+ $this->assertAccess($userCanRead, self::UNIT_TEST_USER1, '{DAV:}read', $acl);
+ $this->assertAccess($userCanWrite, self::UNIT_TEST_USER1, '{DAV:}write', $acl);
+
+ // delete the calendar
+ $this->dispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::callback(function ($event) {
+ return $event instanceof CalendarDeletedEvent;
+ }));
+ $this->backend->deleteCalendar($calendars[0]['id'], true);
+ $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+ self::assertEmpty($calendars);
+ }
+
+ public function testCalendarObjectsOperations(): void {
+ $calendarId = $this->createTestCalendar();
+
+ // create a card
+ $uri = static::getUniqueID('calobj');
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+ // get all the calendar objects
+ $calendarObjects = $this->backend->getCalendarObjects($calendarId);
+ $this->assertCount(1, $calendarObjects);
+ $this->assertEquals($calendarId, $calendarObjects[0]['calendarid']);
+ $this->assertArrayHasKey('classification', $calendarObjects[0]);
+
+ // get the calendar objects
+ $calendarObject = $this->backend->getCalendarObject($calendarId, $uri);
+ $this->assertNotNull($calendarObject);
+ $this->assertArrayHasKey('id', $calendarObject);
+ $this->assertArrayHasKey('uri', $calendarObject);
+ $this->assertArrayHasKey('lastmodified', $calendarObject);
+ $this->assertArrayHasKey('etag', $calendarObject);
+ $this->assertArrayHasKey('size', $calendarObject);
+ $this->assertArrayHasKey('classification', $calendarObject);
+ $this->assertArrayHasKey('{' . SharingPlugin::NS_NEXTCLOUD . '}deleted-at', $calendarObject);
+ $this->assertEquals($calData, $calendarObject['calendardata']);
+
+ // update the card
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->updateCalendarObject($calendarId, $uri, $calData);
+ $calendarObject = $this->backend->getCalendarObject($calendarId, $uri);
+ $this->assertEquals($calData, $calendarObject['calendardata']);
+
+ // delete the card
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->deleteCalendarObject($calendarId, $uri);
+ $calendarObjects = $this->backend->getCalendarObjects($calendarId);
+ $this->assertCount(0, $calendarObjects);
+ }
+
+
+ public function testMultipleCalendarObjectsWithSameUID(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+ $this->expectExceptionMessage('Calendar object with uid already exists in this calendar collection.');
+
+ $calendarId = $this->createTestCalendar();
+
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-1
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $uri0 = static::getUniqueID('event');
+ $uri1 = static::getUniqueID('event');
+ $this->backend->createCalendarObject($calendarId, $uri0, $calData);
+ $this->backend->createCalendarObject($calendarId, $uri1, $calData);
+ }
+
+ public function testMultiCalendarObjects(): void {
+ $calendarId = $this->createTestCalendar();
+
+ // create an event
+ $calData = [];
+ $calData[] = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-1
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $calData[] = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-2
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $calData[] = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-3
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $uri0 = static::getUniqueID('card');
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->createCalendarObject($calendarId, $uri0, $calData[0]);
+ $uri1 = static::getUniqueID('card');
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->createCalendarObject($calendarId, $uri1, $calData[1]);
+ $uri2 = static::getUniqueID('card');
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->createCalendarObject($calendarId, $uri2, $calData[2]);
+
+ // get all the cards
+ $calendarObjects = $this->backend->getCalendarObjects($calendarId);
+ $this->assertCount(3, $calendarObjects);
+
+ // get the cards
+ $calendarObjects = $this->backend->getMultipleCalendarObjects($calendarId, [$uri1, $uri2]);
+ $this->assertCount(2, $calendarObjects);
+ foreach ($calendarObjects as $card) {
+ $this->assertArrayHasKey('id', $card);
+ $this->assertArrayHasKey('uri', $card);
+ $this->assertArrayHasKey('lastmodified', $card);
+ $this->assertArrayHasKey('etag', $card);
+ $this->assertArrayHasKey('size', $card);
+ $this->assertArrayHasKey('classification', $card);
+ }
+
+ usort($calendarObjects, function ($a, $b) {
+ return $a['id'] - $b['id'];
+ });
+
+ $this->assertEquals($calData[1], $calendarObjects[0]['calendardata']);
+ $this->assertEquals($calData[2], $calendarObjects[1]['calendardata']);
+
+ // delete the card
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->deleteCalendarObject($calendarId, $uri0);
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->deleteCalendarObject($calendarId, $uri1);
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+ $this->backend->deleteCalendarObject($calendarId, $uri2);
+ $calendarObjects = $this->backend->getCalendarObjects($calendarId);
+ $this->assertCount(0, $calendarObjects);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesCalendarQueryParameters')]
+ public function testCalendarQuery($expectedEventsInResult, $propFilters, $compFilter): void {
+ $calendarId = $this->createTestCalendar();
+ $events = [];
+ $events[0] = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z');
+ $events[1] = $this->createEvent($calendarId, '20130912T150000Z', '20130912T170000Z');
+ $events[2] = $this->createEvent($calendarId, '20130912T173000Z', '20130912T220000Z');
+ if (PHP_INT_SIZE > 8) {
+ $events[3] = $this->createEvent($calendarId, '21130912T130000Z', '22130912T130000Z');
+ } else {
+ /* On 32bit we do not support events after 2038 */
+ $events[3] = $this->createEvent($calendarId, '20370912T130000Z', '20370912T130000Z');
+ }
+
+ $result = $this->backend->calendarQuery($calendarId, [
+ 'name' => '',
+ 'prop-filters' => $propFilters,
+ 'comp-filters' => $compFilter
+ ]);
+
+ $expectedEventsInResult = array_map(function ($index) use ($events) {
+ return $events[$index];
+ }, $expectedEventsInResult);
+ $this->assertEqualsCanonicalizing($expectedEventsInResult, $result);
+ }
+
+ public function testGetCalendarObjectByUID(): void {
+ $calendarId = $this->createTestCalendar();
+ $uri = static::getUniqueID('calobj');
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+ $co = $this->backend->getCalendarObjectByUID(self::UNIT_TEST_USER, '47d15e3ec8');
+ $this->assertNotNull($co);
+ }
+
+ public static function providesCalendarQueryParameters(): array {
+ return [
+ 'all' => [[0, 1, 2, 3], [], []],
+ 'only-todos' => [[], ['name' => 'VTODO'], []],
+ 'only-events' => [[0, 1, 2, 3], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => null], 'prop-filters' => []]],],
+ 'start' => [[1, 2, 3], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],],
+ 'end' => [[0], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC'))], 'prop-filters' => []]],],
+ 'future' => [[3], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2036-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],],
+ ];
+ }
+
+ public function testCalendarSynchronization(): void {
+
+ // construct calendar for testing
+ $calendarId = $this->createTestCalendar();
+
+ /** test fresh sync state with NO events in calendar */
+ // construct test state
+ $stateTest = ['syncToken' => 1, 'added' => [], 'modified' => [], 'deleted' => []];
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '', 1);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test fresh sync state with NO events in calendar');
+
+ /** test delta sync state with NO events in calendar */
+ // construct test state
+ $stateTest = ['syncToken' => 1, 'added' => [], 'modified' => [], 'deleted' => []];
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '2', 1);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with NO events in calendar');
+
+ /** add events to calendar */
+ $event1 = $this->createEvent($calendarId, '20240701T130000Z', '20240701T140000Z');
+ $event2 = $this->createEvent($calendarId, '20240701T140000Z', '20240701T150000Z');
+ $event3 = $this->createEvent($calendarId, '20240701T150000Z', '20240701T160000Z');
+
+ /** test fresh sync state with events in calendar */
+ // construct expected state
+ $stateTest = ['syncToken' => 4, 'added' => [$event1, $event2, $event3], 'modified' => [], 'deleted' => []];
+ sort($stateTest['added']);
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '', 1);
+ // sort live state results
+ sort($stateLive['added']);
+ sort($stateLive['modified']);
+ sort($stateLive['deleted']);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test fresh sync state with events in calendar');
+
+ /** test delta sync state with events in calendar */
+ // construct expected state
+ $stateTest = ['syncToken' => 4, 'added' => [$event2, $event3], 'modified' => [], 'deleted' => []];
+ sort($stateTest['added']);
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '2', 1);
+ // sort live state results
+ sort($stateLive['added']);
+ sort($stateLive['modified']);
+ sort($stateLive['deleted']);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with events in calendar');
+
+ /** modify/delete events in calendar */
+ $this->deleteEvent($calendarId, $event1);
+ $this->modifyEvent($calendarId, $event2, '20250701T140000Z', '20250701T150000Z');
+
+ /** test fresh sync state with modified/deleted events in calendar */
+ // construct expected state
+ $stateTest = ['syncToken' => 6, 'added' => [$event2, $event3], 'modified' => [], 'deleted' => []];
+ sort($stateTest['added']);
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '', 1);
+ // sort live state results
+ sort($stateLive['added']);
+ sort($stateLive['modified']);
+ sort($stateLive['deleted']);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test fresh sync state with modified/deleted events in calendar');
+
+ /** test delta sync state with modified/deleted events in calendar */
+ // construct expected state
+ $stateTest = ['syncToken' => 6, 'added' => [$event3], 'modified' => [$event2], 'deleted' => [$event1]];
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '3', 1);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with modified/deleted events in calendar');
+
+ /** test delta sync state with modified/deleted events in calendar and invalid token */
+ // construct expected state
+ $stateTest = ['syncToken' => 6, 'added' => [], 'modified' => [], 'deleted' => []];
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '6', 1);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with modified/deleted events in calendar and invalid token');
+
+ }
+
+ public function testPublications(): void {
+ $this->dispatcher->expects(self::atLeastOnce())
+ ->method('dispatchTyped');
+
+ $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', []);
+
+ $calendarInfo = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)[0];
+
+ /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject $l10n */
+ $l10n = $this->createMock(IL10N::class);
+ $config = $this->createMock(IConfig::class);
+ $logger = $this->createMock(\Psr\Log\LoggerInterface::class);
+ $calendar = new Calendar($this->backend, $calendarInfo, $l10n, $config, $logger);
+ $calendar->setPublishStatus(true);
+ $this->assertNotEquals(false, $calendar->getPublishStatus());
+
+ $publicCalendars = $this->backend->getPublicCalendars();
+ $this->assertCount(1, $publicCalendars);
+ $this->assertEquals(true, $publicCalendars[0]['{http://owncloud.org/ns}public']);
+ $this->assertEquals('User\'s displayname', $publicCalendars[0]['{http://nextcloud.com/ns}owner-displayname']);
+
+ $publicCalendarURI = $publicCalendars[0]['uri'];
+ $publicCalendar = $this->backend->getPublicCalendar($publicCalendarURI);
+ $this->assertEquals(true, $publicCalendar['{http://owncloud.org/ns}public']);
+
+ $calendar->setPublishStatus(false);
+ $this->assertEquals(false, $calendar->getPublishStatus());
+
+ $this->expectException(NotFound::class);
+ $this->backend->getPublicCalendar($publicCalendarURI);
+ }
+
+ public function testSubscriptions(): void {
+ $id = $this->backend->createSubscription(self::UNIT_TEST_USER, 'Subscription', [
+ '{http://calendarserver.org/ns/}source' => new Href('test-source'),
+ '{http://apple.com/ns/ical/}calendar-color' => '#1C4587',
+ '{http://calendarserver.org/ns/}subscribed-strip-todos' => ''
+ ]);
+
+ $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+ $this->assertCount(1, $subscriptions);
+ $this->assertEquals('#1C4587', $subscriptions[0]['{http://apple.com/ns/ical/}calendar-color']);
+ $this->assertEquals(true, $subscriptions[0]['{http://calendarserver.org/ns/}subscribed-strip-todos']);
+ $this->assertEquals($id, $subscriptions[0]['id']);
+
+ $patch = new PropPatch([
+ '{DAV:}displayname' => 'Unit test',
+ '{http://apple.com/ns/ical/}calendar-color' => '#ac0606',
+ ]);
+ $this->backend->updateSubscription($id, $patch);
+ $patch->commit();
+
+ $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+ $this->assertCount(1, $subscriptions);
+ $this->assertEquals($id, $subscriptions[0]['id']);
+ $this->assertEquals('Unit test', $subscriptions[0]['{DAV:}displayname']);
+ $this->assertEquals('#ac0606', $subscriptions[0]['{http://apple.com/ns/ical/}calendar-color']);
+
+ $this->backend->deleteSubscription($id);
+ $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+ $this->assertCount(0, $subscriptions);
+ }
+
+ public static function providesSchedulingData(): array {
+ $data = <<<EOS
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 3.5.0//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:Europe/Warsaw
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20170320T131655Z
+LAST-MODIFIED:20170320T135019Z
+DTSTAMP:20170320T135019Z
+UID:7e908a6d-4c4e-48d7-bd62-59ab80fbf1a3
+SUMMARY:TEST Z pg_escape_bytea
+ORGANIZER;RSVP=TRUE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:k.klimczak@gromar.e
+ u
+ATTENDEE;RSVP=TRUE;CN=Zuzanna Leszek;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICI
+ PANT:mailto:z.leszek@gromar.eu
+ATTENDEE;RSVP=TRUE;CN=Marcin Pisarski;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTIC
+ IPANT:mailto:m.pisarski@gromar.eu
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:klimcz
+ ak.k@gmail.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:k_klim
+ czak@tlen.pl
+DTSTART;TZID=Europe/Warsaw:20170325T150000
+DTEND;TZID=Europe/Warsaw:20170325T160000
+TRANSP:OPAQUE
+DESCRIPTION:Magiczna treść uzyskana za pomocą magicznego proszku.\n\nę
+ żźćńłóÓŻŹĆŁĘ€śśśŚŚ\n \,\,))))))))\;\,\n
+ __))))))))))))))\,\n \\|/ -\\(((((''''((((((((.\n -*-==///
+ ///(('' . `))))))\,\n /|\\ ))| o \;-. '(((((
+ \,(\,\n ( `| / ) \;))))'
+ \,_))^\;(~\n | | | \,))((((_ _____-
+ -----~~~-. %\,\;(\;(>'\;'~\n o_)\; \; )))(((` ~---
+ ~ `:: \\ %%~~)(v\;(`('~\n \; ''''````
+ `: `:::|\\\,__\,%% )\;`'\; ~\n | _
+ ) / `:|`----' `-'\n ______/\\/~ |
+ / /\n /~\;\;.____/\;\;' / ___--\
+ ,-( `\;\;\;/\n / // _\;______\;'------~~~~~ /\;\;/\\ /\n
+ // | | / \; \\\;\;\,\\\n (<_ | \;
+ /'\,/-----' _>\n \\_| ||_
+ //~\;~~~~~~~~~\n `\\_| (\,~~ -Tua Xiong\n
+ \\~\\\n
+ ~~\n\n
+SEQUENCE:1
+X-MOZ-GENERATION:1
+END:VEVENT
+END:VCALENDAR
+EOS;
+
+ return [
+ 'no data' => [''],
+ 'failing on postgres' => [$data]
+ ];
+ }
+
+ /**
+ * @param $objectData
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesSchedulingData')]
+ public function testScheduling($objectData): void {
+ $this->backend->createSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule', $objectData);
+
+ $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER);
+ $this->assertCount(1, $sos);
+
+ $so = $this->backend->getSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule');
+ $this->assertNotNull($so);
+
+ $this->backend->deleteSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule');
+
+ $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER);
+ $this->assertCount(0, $sos);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesCalDataForGetDenormalizedData')]
+ public function testGetDenormalizedData($expected, $key, $calData): void {
+ try {
+ $actual = $this->backend->getDenormalizedData($calData);
+ $this->assertEquals($expected, $actual[$key]);
+ } catch (\Throwable $e) {
+ if (($e->getMessage() === 'Epoch doesn\'t fit in a PHP integer') && (PHP_INT_SIZE < 8)) {
+ $this->markTestSkipped('This fail on 32bits because of PHP limitations in DateTime');
+ }
+ throw $e;
+ }
+ }
+
+ public static function providesCalDataForGetDenormalizedData(): array {
+ return [
+ 'first occurrence before unix epoch starts' => [0, 'firstOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:413F269B-B51B-46B1-AFB6-40055C53A4DC\r\nDTSTAMP:20160309T095056Z\r\nDTSTART;VALUE=DATE:16040222\r\nDTEND;VALUE=DATE:16040223\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:SUMMARY\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+ 'no first occurrence because yearly' => [null, 'firstOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:413F269B-B51B-46B1-AFB6-40055C53A4DC\r\nDTSTAMP:20160309T095056Z\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:SUMMARY\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+
+ 'last occurrence is max when only last VEVENT in group is weekly' => [(new DateTime(CalDavBackend::MAX_DATE))->getTimestamp(), 'lastOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.3.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20200812T103000\r\nDTEND;TZID=America/Los_Angeles:20200812T110000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdfasdfasdf@google.com\r\nRECURRENCE-ID;TZID=America/Los_Angeles:20200811T123000\r\nCREATED:20200626T181848Z\r\nLAST-MODIFIED:20200922T192707Z\r\nSUMMARY:Weekly 1:1\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20200728T123000\r\nDTEND;TZID=America/Los_Angeles:20200728T130000\r\nEXDATE;TZID=America/Los_Angeles:20200818T123000\r\nRRULE:FREQ=WEEKLY;BYDAY=TU\r\nDTSTAMP:20200927T180638Z\r\nUID:asdfasdfasdf@google.com\r\nCREATED:20200626T181848Z\r\nDESCRIPTION:Setting up recurring time on our calendars\r\nLAST-MODIFIED:20200922T192707Z\r\nSUMMARY:Weekly 1:1\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+
+ 'last occurrence before unix epoch starts' => [0, 'lastOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.3.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:19110324\r\nDTEND;VALUE=DATE:19110325\r\nDTSTAMP:20200927T180638Z\r\nUID:asdfasdfasdf@google.com\r\nCREATED:20200626T181848Z\r\nDESCRIPTION:Very old event\r\nLAST-MODIFIED:20200922T192707Z\r\nSUMMARY:Some old event\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+
+ 'first occurrence is found when not first VEVENT in group' => [(new DateTime('2020-09-01T110000', new DateTimeZone('America/Los_Angeles')))->getTimestamp(), 'firstOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.3.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20201013T110000\r\nDTEND;TZID=America/Los_Angeles:20201013T120000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdf0000@google.com\r\nRECURRENCE-ID;TZID=America/Los_Angeles:20201013T110000\r\nCREATED:20160330T034726Z\r\nLAST-MODIFIED:20200925T042014Z\r\nSTATUS:CONFIRMED\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20200901T110000\r\nDTEND;TZID=America/Los_Angeles:20200901T120000\r\nRRULE:FREQ=WEEKLY;BYDAY=TU\r\nEXDATE;TZID=America/Los_Angeles:20200922T110000\r\nEXDATE;TZID=America/Los_Angeles:20200915T110000\r\nEXDATE;TZID=America/Los_Angeles:20200908T110000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdf0000@google.com\r\nCREATED:20160330T034726Z\r\nLAST-MODIFIED:20200915T162810Z\r\nSTATUS:CONFIRMED\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+
+ 'CLASS:PRIVATE' => [CalDavBackend::CLASSIFICATION_PRIVATE, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:PRIVATE\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"],
+
+ 'CLASS:PUBLIC' => [CalDavBackend::CLASSIFICATION_PUBLIC, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:PUBLIC\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"],
+
+ 'CLASS:CONFIDENTIAL' => [CalDavBackend::CLASSIFICATION_CONFIDENTIAL, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:CONFIDENTIAL\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"],
+
+ 'no class set -> public' => [CalDavBackend::CLASSIFICATION_PUBLIC, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nTRANSP:OPAQUE\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"],
+
+ 'unknown class -> private' => [CalDavBackend::CLASSIFICATION_PRIVATE, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:VERTRAULICH\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"],
+ ];
+ }
+
+ public function testCalendarSearch(): void {
+ $calendarId = $this->createTestCalendar();
+
+ $uri = static::getUniqueID('calobj');
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+ $search1 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [
+ 'comps' => [
+ 'VEVENT',
+ 'VTODO'
+ ],
+ 'props' => [
+ 'SUMMARY',
+ 'LOCATION'
+ ],
+ 'search-term' => 'Test',
+ ]);
+ $this->assertEquals(count($search1), 1);
+
+
+ // update the card
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:123 Event 🙈
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+ATTENDEE;CN=test:mailto:foo@bar.com
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->updateCalendarObject($calendarId, $uri, $calData);
+
+ $search2 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [
+ 'comps' => [
+ 'VEVENT',
+ 'VTODO'
+ ],
+ 'props' => [
+ 'SUMMARY',
+ 'LOCATION'
+ ],
+ 'search-term' => 'Test',
+ ]);
+ $this->assertEquals(count($search2), 0);
+
+ $search3 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [
+ 'comps' => [
+ 'VEVENT',
+ 'VTODO'
+ ],
+ 'props' => [
+ 'SUMMARY',
+ 'LOCATION'
+ ],
+ 'params' => [
+ [
+ 'property' => 'ATTENDEE',
+ 'parameter' => 'CN'
+ ]
+ ],
+ 'search-term' => 'Test',
+ ]);
+ $this->assertEquals(count($search3), 1);
+
+ // t matches both summary and attendee's CN, but we want unique results
+ $search4 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [
+ 'comps' => [
+ 'VEVENT',
+ 'VTODO'
+ ],
+ 'props' => [
+ 'SUMMARY',
+ 'LOCATION'
+ ],
+ 'params' => [
+ [
+ 'property' => 'ATTENDEE',
+ 'parameter' => 'CN'
+ ]
+ ],
+ 'search-term' => 't',
+ ]);
+ $this->assertEquals(count($search4), 1);
+
+ $this->backend->deleteCalendarObject($calendarId, $uri);
+
+ $search5 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [
+ 'comps' => [
+ 'VEVENT',
+ 'VTODO'
+ ],
+ 'props' => [
+ 'SUMMARY',
+ 'LOCATION'
+ ],
+ 'params' => [
+ [
+ 'property' => 'ATTENDEE',
+ 'parameter' => 'CN'
+ ]
+ ],
+ 'search-term' => 't',
+ ]);
+ $this->assertEquals(count($search5), 0);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
+ public function testSearch(bool $isShared, array $searchOptions, int $count): void {
+ $calendarId = $this->createTestCalendar();
+
+ $uris = [];
+ $calData = [];
+
+ $uris[] = static::getUniqueID('calobj');
+ $calData[] = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-1
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $uris[] = static::getUniqueID('calobj');
+ $calData[] = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-2
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:123
+LOCATION:Test
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $uris[] = static::getUniqueID('calobj');
+ $calData[] = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-3
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:123
+ATTENDEE;CN=test:mailto:foo@bar.com
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PRIVATE
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $uris[] = static::getUniqueID('calobj');
+ $calData[] = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-4
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:123
+ATTENDEE;CN=foobar:mailto:test@bar.com
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:CONFIDENTIAL
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $uriCount = count($uris);
+ for ($i = 0; $i < $uriCount; $i++) {
+ $this->backend->createCalendarObject($calendarId,
+ $uris[$i], $calData[$i]);
+ }
+
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => $isShared ? 'user2' : 'user1',
+ ];
+
+ $result = $this->backend->search($calendarInfo, 'Test',
+ ['SUMMARY', 'LOCATION', 'ATTENDEE'], $searchOptions, null, null);
+
+ $this->assertCount($count, $result);
+ }
+
+ public static function searchDataProvider(): array {
+ return [
+ [false, [], 4],
+ [true, ['timerange' => ['start' => new DateTime('2013-09-12 13:00:00'), 'end' => new DateTime('2013-09-12 14:00:00')]], 2],
+ [true, ['timerange' => ['start' => new DateTime('2013-09-12 15:00:00'), 'end' => new DateTime('2013-09-12 16:00:00')]], 0],
+ ];
+ }
+
+ public function testSameUriSameIdForDifferentCalendarTypes(): void {
+ $calendarId = $this->createTestCalendar();
+ $subscriptionId = $this->createTestSubscription();
+
+ $uri = static::getUniqueID('calobj');
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $calData2 = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event 123
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+ $this->backend->createCalendarObject($subscriptionId, $uri, $calData2, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
+
+ $this->assertEquals($calData, $this->backend->getCalendarObject($calendarId, $uri, CalDavBackend::CALENDAR_TYPE_CALENDAR)['calendardata']);
+ $this->assertEquals($calData2, $this->backend->getCalendarObject($subscriptionId, $uri, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION)['calendardata']);
+ }
+
+ public function testPurgeAllCachedEventsForSubscription(): void {
+ $subscriptionId = $this->createTestSubscription();
+ $uri = static::getUniqueID('calobj');
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $this->backend->createCalendarObject($subscriptionId, $uri, $calData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
+ $this->backend->purgeAllCachedEventsForSubscription($subscriptionId);
+
+ $this->assertEquals(null, $this->backend->getCalendarObject($subscriptionId, $uri, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION));
+ }
+
+ public function testCalendarMovement(): void {
+ $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', []);
+
+ $this->assertCount(1, $this->backend->getCalendarsForUser(self::UNIT_TEST_USER));
+
+ $calendarInfoUser = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)[0];
+
+ $this->backend->moveCalendar('Example', self::UNIT_TEST_USER, self::UNIT_TEST_USER1);
+ $this->assertCount(0, $this->backend->getCalendarsForUser(self::UNIT_TEST_USER));
+ $this->assertCount(1, $this->backend->getCalendarsForUser(self::UNIT_TEST_USER1));
+
+ $calendarInfoUser1 = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER1)[0];
+ $this->assertEquals($calendarInfoUser['id'], $calendarInfoUser1['id']);
+ $this->assertEquals($calendarInfoUser['uri'], $calendarInfoUser1['uri']);
+ }
+
+ public function testSearchPrincipal(): void {
+ $myPublic = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:My Test (public)
+CLASS:PUBLIC
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-1
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $myPrivate = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:My Test (private)
+CLASS:PRIVATE
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-2
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $myConfidential = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:My Test (confidential)
+CLASS:CONFIDENTIAL
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-3
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $sharerPublic = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:Sharer Test (public)
+CLASS:PUBLIC
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-4
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $sharerPrivate = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:Sharer Test (private)
+CLASS:PRIVATE
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-5
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $sharerConfidential = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:Sharer Test (confidential)
+CLASS:CONFIDENTIAL
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-6
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $l10n = $this->createMock(IL10N::class);
+ $l10n
+ ->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($text, $parameters = []) {
+ return vsprintf($text, $parameters);
+ });
+ $config = $this->createMock(IConfig::class);
+ $this->userManager->expects($this->any())
+ ->method('userExists')
+ ->willReturn(true);
+ $this->groupManager->expects($this->any())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->principal->expects(self::atLeastOnce())
+ ->method('findByUri')
+ ->willReturn(self::UNIT_TEST_USER);
+
+ $me = self::UNIT_TEST_USER;
+ $sharer = self::UNIT_TEST_USER1;
+ $this->backend->createCalendar($me, 'calendar-uri-me', []);
+ $this->backend->createCalendar($sharer, 'calendar-uri-sharer', []);
+
+ $myCalendars = $this->backend->getCalendarsForUser($me);
+ $this->assertCount(1, $myCalendars);
+
+ $sharerCalendars = $this->backend->getCalendarsForUser($sharer);
+ $this->assertCount(1, $sharerCalendars);
+
+ $logger = $this->createMock(\Psr\Log\LoggerInterface::class);
+ $sharerCalendar = new Calendar($this->backend, $sharerCalendars[0], $l10n, $config, $logger);
+ $this->backend->updateShares($sharerCalendar, [
+ [
+ 'href' => 'principal:' . $me,
+ 'readOnly' => false,
+ ],
+ ], []);
+
+ $this->assertCount(2, $this->backend->getCalendarsForUser($me));
+
+ $this->backend->createCalendarObject($myCalendars[0]['id'], 'event0.ics', $myPublic);
+ $this->backend->createCalendarObject($myCalendars[0]['id'], 'event1.ics', $myPrivate);
+ $this->backend->createCalendarObject($myCalendars[0]['id'], 'event2.ics', $myConfidential);
+
+ $this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event3.ics', $sharerPublic);
+ $this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event4.ics', $sharerPrivate);
+ $this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event5.ics', $sharerConfidential);
+
+ $mySearchResults = $this->backend->searchPrincipalUri($me, 'Test', ['VEVENT'], ['SUMMARY'], []);
+ $sharerSearchResults = $this->backend->searchPrincipalUri($sharer, 'Test', ['VEVENT'], ['SUMMARY'], []);
+
+ $this->assertCount(4, $mySearchResults);
+ $this->assertCount(3, $sharerSearchResults);
+
+ $this->assertEquals($myPublic, $mySearchResults[0]['calendardata']);
+ $this->assertEquals($myPrivate, $mySearchResults[1]['calendardata']);
+ $this->assertEquals($myConfidential, $mySearchResults[2]['calendardata']);
+ $this->assertEquals($sharerPublic, $mySearchResults[3]['calendardata']);
+
+ $this->assertEquals($sharerPublic, $sharerSearchResults[0]['calendardata']);
+ $this->assertEquals($sharerPrivate, $sharerSearchResults[1]['calendardata']);
+ $this->assertEquals($sharerConfidential, $sharerSearchResults[2]['calendardata']);
+ }
+
+ /**
+ * @throws \OCP\DB\Exception
+ * @throws \Sabre\DAV\Exception\BadRequest
+ */
+ public function testPruneOutdatedSyncTokens(): void {
+ $calendarId = $this->createTestCalendar();
+ $changes = $this->backend->getChangesForCalendar($calendarId, '', 1);
+ $syncToken = $changes['syncToken'];
+
+ $uri = static::getUniqueID('calobj');
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+ // update the card
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:123 Event 🙈
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+ATTENDEE;CN=test:mailto:foo@bar.com
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->updateCalendarObject($calendarId, $uri, $calData);
+
+ // Keep everything
+ $deleted = $this->backend->pruneOutdatedSyncTokens(0, 0);
+ self::assertSame(0, $deleted);
+
+ $deleted = $this->backend->pruneOutdatedSyncTokens(0, time());
+ // At least one from the object creation and one from the object update
+ $this->assertGreaterThanOrEqual(2, $deleted);
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 1);
+ $this->assertEmpty($changes['added']);
+ $this->assertEmpty($changes['modified']);
+ $this->assertEmpty($changes['deleted']);
+
+ // Test that objects remain
+
+ // Currently changes are empty
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100);
+ $this->assertEquals(0, count($changes['added'] + $changes['modified'] + $changes['deleted']));
+
+ // Create card
+ $uri = static::getUniqueID('calobj');
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20230910T125139Z
+UID:47d15e3ec9
+LAST-MODIFIED;VALUE=DATE-TIME:20230910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20230910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20230912T130000Z
+DTEND;VALUE=DATE-TIME:20230912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+ // We now have one add
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100);
+ $this->assertEquals(1, count($changes['added']));
+ $this->assertEmpty($changes['modified']);
+ $this->assertEmpty($changes['deleted']);
+
+ // update the card
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20230910T125139Z
+UID:47d15e3ec9
+LAST-MODIFIED;VALUE=DATE-TIME:20230910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20230910T125139Z
+SUMMARY:123 Event 🙈
+DTSTART;VALUE=DATE-TIME:20230912T130000Z
+DTEND;VALUE=DATE-TIME:20230912T140000Z
+ATTENDEE;CN=test:mailto:foo@bar.com
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->updateCalendarObject($calendarId, $uri, $calData);
+
+ // One add, one modify, but shortened to modify
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100);
+ $this->assertEmpty($changes['added']);
+ $this->assertEquals(1, count($changes['modified']));
+ $this->assertEmpty($changes['deleted']);
+
+ // Delete all but last change
+ $deleted = $this->backend->pruneOutdatedSyncTokens(1, time());
+ $this->assertEquals(1, $deleted); // We had two changes before, now one
+
+ // Only update should remain
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100);
+ $this->assertEmpty($changes['added']);
+ $this->assertEquals(1, count($changes['modified']));
+ $this->assertEmpty($changes['deleted']);
+
+ // Check that no crash occurs when prune is called without current changes
+ $deleted = $this->backend->pruneOutdatedSyncTokens(1, time());
+ self::assertSame(0, $deleted);
+ }
+
+ public function testSearchAndExpandRecurrences(): void {
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ ];
+
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+PRODID:-//IDN nextcloud.com//Calendar app 4.5.0-alpha.2//EN
+CALSCALE:GREGORIAN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20230921T133401Z
+DTSTAMP:20230921T133448Z
+LAST-MODIFIED:20230921T133448Z
+SEQUENCE:2
+UID:7b7d5d12-683c-48ce-973a-b3e1cb0bae2a
+DTSTART;VALUE=DATE:20230912
+DTEND;VALUE=DATE:20230913
+STATUS:CONFIRMED
+SUMMARY:Daily Event
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $uri = static::getUniqueID('calobj');
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+ $start = new DateTimeImmutable('2023-09-20T00:00:00Z');
+ $end = $start->add(new DateInterval('P14D'));
+
+ $results = $this->backend->search(
+ $calendarInfo,
+ '',
+ [],
+ [
+ 'timerange' => [
+ 'start' => $start,
+ 'end' => $end,
+ ]
+ ],
+ null,
+ null,
+ );
+
+ $this->assertCount(1, $results);
+ $this->assertCount(14, $results[0]['objects']);
+ foreach ($results as $result) {
+ foreach ($result['objects'] as $object) {
+ $this->assertEquals($object['UID'][0], '7b7d5d12-683c-48ce-973a-b3e1cb0bae2a');
+ $this->assertEquals($object['SUMMARY'][0], 'Daily Event');
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $object['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+ $this->assertLessThanOrEqual(
+ $end->getTimestamp(),
+ $object['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting after requested end',
+ );
+ }
+ }
+ }
+
+ public function testRestoreChanges(): void {
+ $calendarId = $this->createTestCalendar();
+ $uri1 = static::getUniqueID('calobj1') . '.ics';
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->createCalendarObject($calendarId, $uri1, $calData);
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event – UPDATED
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->updateCalendarObject($calendarId, $uri1, $calData);
+ $uri2 = static::getUniqueID('calobj2') . '.ics';
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec9
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->createCalendarObject($calendarId, $uri2, $calData);
+ $changesBefore = $this->backend->getChangesForCalendar($calendarId, null, 1);
+ $this->backend->deleteCalendarObject($calendarId, $uri2);
+ $uri3 = static::getUniqueID('calobj3') . '.ics';
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3e10
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->createCalendarObject($calendarId, $uri3, $calData);
+ $deleteChanges = $this->db->getQueryBuilder();
+ $deleteChanges->delete('calendarchanges')
+ ->where($deleteChanges->expr()->eq('calendarid', $deleteChanges->createNamedParameter($calendarId)));
+ $deleteChanges->executeStatement();
+
+ $this->backend->restoreChanges($calendarId);
+
+ $changesAfter = $this->backend->getChangesForCalendar($calendarId, $changesBefore['syncToken'], 1);
+ self::assertEquals([], $changesAfter['added']);
+ self::assertEqualsCanonicalizing([$uri1, $uri3], $changesAfter['modified']);
+ self::assertEquals([$uri2], $changesAfter['deleted']);
+ }
+
+ public function testSearchWithLimitAndTimeRange(): void {
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ ];
+
+ $testFiles = [
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-1.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-2.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-3.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-4.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics',
+ ];
+
+ foreach ($testFiles as $testFile) {
+ $objectUri = static::getUniqueID('search-limit-timerange-');
+ $calendarData = \file_get_contents($testFile);
+ $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
+ }
+
+ $start = new DateTimeImmutable('2024-05-06T00:00:00Z');
+ $end = $start->add(new DateInterval('P14D'));
+
+ $results = $this->backend->search(
+ $calendarInfo,
+ '',
+ [],
+ [
+ 'timerange' => [
+ 'start' => $start,
+ 'end' => $end,
+ ]
+ ],
+ 4,
+ null,
+ );
+
+ $this->assertCount(2, $results);
+
+ $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $results[0]['objects'][0]['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+
+ $this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]);
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $results[1]['objects'][0]['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+ }
+
+ public function testSearchWithLimitAndTimeRangeShouldNotReturnMoreObjectsThenLimit(): void {
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ ];
+
+ $testFiles = [
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-1.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-2.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-3.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-4.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics',
+ ];
+
+ foreach ($testFiles as $testFile) {
+ $objectUri = static::getUniqueID('search-limit-timerange-');
+ $calendarData = \file_get_contents($testFile);
+ $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
+ }
+
+ $start = new DateTimeImmutable('2024-05-06T00:00:00Z');
+ $end = $start->add(new DateInterval('P14D'));
+
+ $results = $this->backend->search(
+ $calendarInfo,
+ '',
+ [],
+ [
+ 'timerange' => [
+ 'start' => $start,
+ 'end' => $end,
+ ]
+ ],
+ 1,
+ null,
+ );
+
+ $this->assertCount(1, $results);
+
+ $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $results[0]['objects'][0]['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+ }
+
+ public function testSearchWithLimitAndTimeRangeShouldReturnObjectsInTheSameOrder(): void {
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ ];
+
+ $testFiles = [
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-1.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-2.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-3.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-4.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics', // <-- intentional!
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics',
+ ];
+
+ foreach ($testFiles as $testFile) {
+ $objectUri = static::getUniqueID('search-limit-timerange-');
+ $calendarData = \file_get_contents($testFile);
+ $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
+ }
+
+ $start = new DateTimeImmutable('2024-05-06T00:00:00Z');
+ $end = $start->add(new DateInterval('P14D'));
+
+ $results = $this->backend->search(
+ $calendarInfo,
+ '',
+ [],
+ [
+ 'timerange' => [
+ 'start' => $start,
+ 'end' => $end,
+ ]
+ ],
+ 2,
+ null,
+ );
+
+ $this->assertCount(2, $results);
+
+ $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $results[0]['objects'][0]['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+
+ $this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]);
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $results[1]['objects'][0]['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+ }
+
+ public function testSearchShouldReturnObjectsInTheSameOrderMissingDate(): void {
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ ];
+
+ $testFiles = [
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics', // <-- intentional!
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-missing-start-1.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-missing-start-2.ics',
+ ];
+
+ foreach ($testFiles as $testFile) {
+ $objectUri = static::getUniqueID('search-return-objects-in-same-order-');
+ $calendarData = \file_get_contents($testFile);
+ $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
+ }
+
+ $results = $this->backend->search(
+ $calendarInfo,
+ '',
+ [],
+ [],
+ 4,
+ null,
+ );
+
+ $this->assertCount(4, $results);
+
+ $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
+ $this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]);
+ $this->assertEquals('Missing DTSTART 1', $results[2]['objects'][0]['SUMMARY'][0]);
+ $this->assertEquals('Missing DTSTART 2', $results[3]['objects'][0]['SUMMARY'][0]);
+ }
+
+ public function testUnshare(): void {
+ $principalGroup = 'principal:' . self::UNIT_TEST_GROUP;
+ $principalUser = 'principal:' . self::UNIT_TEST_USER;
+
+ $l10n = $this->createMock(IL10N::class);
+ $l10n->method('t')
+ ->willReturnCallback(fn ($text, $parameters = []) => vsprintf($text, $parameters));
+ $config = $this->createMock(IConfig::class);
+ $logger = new NullLogger();
+
+ $this->principal->expects($this->exactly(2))
+ ->method('findByUri')
+ ->willReturnMap([
+ [$principalGroup, '', self::UNIT_TEST_GROUP],
+ [$principalUser, '', self::UNIT_TEST_USER],
+ ]);
+ $this->groupManager->expects($this->once())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->dispatcher->expects($this->exactly(2))
+ ->method('dispatchTyped');
+
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = $this->backend->getCalendarById($calendarId);
+
+ $calendar = new Calendar($this->backend, $calendarInfo, $l10n, $config, $logger);
+
+ $this->backend->updateShares(
+ shareable: $calendar,
+ add: [
+ ['href' => $principalGroup, 'readOnly' => false]
+ ],
+ remove: []
+ );
+
+ $this->backend->unshare(
+ shareable: $calendar,
+ principal: $principalUser
+ );
+
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php b/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php
new file mode 100644
index 00000000000..e25cc099bd6
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php
@@ -0,0 +1,347 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\AppInfo\PluginManager;
+use OCA\DAV\CalDAV\CachedSubscription;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\Integration\ExternalCalendar;
+use OCA\DAV\CalDAV\Integration\ICalendarProvider;
+use OCA\DAV\CalDAV\Outbox;
+use OCA\DAV\CalDAV\Trashbin\TrashbinHome;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\CalDAV\Schedule\Inbox;
+use Sabre\CalDAV\Subscriptions\Subscription;
+use Sabre\DAV\MkCol;
+use Test\TestCase;
+
+class CalendarHomeTest extends TestCase {
+ private CalDavBackend&MockObject $backend;
+ private array $principalInfo = [];
+ private PluginManager&MockObject $pluginManager;
+ private LoggerInterface&MockObject $logger;
+ private CalendarHome $calendarHome;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->backend = $this->createMock(CalDavBackend::class);
+ $this->principalInfo = [
+ 'uri' => 'user-principal-123',
+ ];
+ $this->pluginManager = $this->createMock(PluginManager::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->calendarHome = new CalendarHome(
+ $this->backend,
+ $this->principalInfo,
+ $this->logger,
+ false
+ );
+
+ // Replace PluginManager with our mock
+ $reflection = new \ReflectionClass($this->calendarHome);
+ $reflectionProperty = $reflection->getProperty('pluginManager');
+ $reflectionProperty->setValue($this->calendarHome, $this->pluginManager);
+ }
+
+ public function testCreateCalendarValidName(): void {
+ /** @var MkCol&MockObject $mkCol */
+ $mkCol = $this->createMock(MkCol::class);
+
+ $mkCol->method('getResourceType')
+ ->willReturn(['{DAV:}collection',
+ '{urn:ietf:params:xml:ns:caldav}calendar']);
+ $mkCol->method('getRemainingValues')
+ ->willReturn(['... properties ...']);
+
+ $this->backend->expects(self::once())
+ ->method('createCalendar')
+ ->with('user-principal-123', 'name123', ['... properties ...']);
+
+ $this->calendarHome->createExtendedCollection('name123', $mkCol);
+ }
+
+ public function testCreateCalendarReservedName(): void {
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+ $this->expectExceptionMessage('The resource you tried to create has a reserved name');
+
+ /** @var MkCol&MockObject $mkCol */
+ $mkCol = $this->createMock(MkCol::class);
+
+ $this->calendarHome->createExtendedCollection('contact_birthdays', $mkCol);
+ }
+
+ public function testCreateCalendarReservedNameAppGenerated(): void {
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+ $this->expectExceptionMessage('The resource you tried to create has a reserved name');
+
+ /** @var MkCol&MockObject $mkCol */
+ $mkCol = $this->createMock(MkCol::class);
+
+ $this->calendarHome->createExtendedCollection('app-generated--example--foo-1', $mkCol);
+ }
+
+ public function testGetChildren():void {
+ $this->backend
+ ->expects(self::once())
+ ->method('getCalendarsForUser')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $calendarPlugin1 = $this->createMock(ICalendarProvider::class);
+ $calendarPlugin1
+ ->expects(self::once())
+ ->method('fetchAllForCalendarHome')
+ ->with('user-principal-123')
+ ->willReturn(['plugin1calendar1', 'plugin1calendar2']);
+
+ $calendarPlugin2 = $this->createMock(ICalendarProvider::class);
+ $calendarPlugin2
+ ->expects(self::once())
+ ->method('fetchAllForCalendarHome')
+ ->with('user-principal-123')
+ ->willReturn(['plugin2calendar1', 'plugin2calendar2']);
+
+ $this->pluginManager
+ ->expects(self::once())
+ ->method('getCalendarPlugins')
+ ->with()
+ ->willReturn([$calendarPlugin1, $calendarPlugin2]);
+
+ $actual = $this->calendarHome->getChildren();
+
+ $this->assertCount(7, $actual);
+ $this->assertInstanceOf(Inbox::class, $actual[0]);
+ $this->assertInstanceOf(Outbox::class, $actual[1]);
+ $this->assertInstanceOf(TrashbinHome::class, $actual[2]);
+ $this->assertEquals('plugin1calendar1', $actual[3]);
+ $this->assertEquals('plugin1calendar2', $actual[4]);
+ $this->assertEquals('plugin2calendar1', $actual[5]);
+ $this->assertEquals('plugin2calendar2', $actual[6]);
+ }
+
+ public function testGetChildNonAppGenerated():void {
+ $this->backend
+ ->expects(self::once())
+ ->method('getCalendarByUri')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
+ ->method('getCalendarsForUser')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->pluginManager
+ ->expects(self::never())
+ ->method('getCalendarPlugins');
+
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ $this->expectExceptionMessage('Node with name \'personal\' could not be found');
+
+ $this->calendarHome->getChild('personal');
+ }
+
+ public function testGetChildAppGenerated():void {
+ $this->backend
+ ->expects(self::once())
+ ->method('getCalendarByUri')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
+ ->method('getCalendarsForUser')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $calendarPlugin1 = $this->createMock(ICalendarProvider::class);
+ $calendarPlugin1
+ ->expects(self::once())
+ ->method('getAppId')
+ ->with()
+ ->willReturn('calendar_plugin_1');
+ $calendarPlugin1
+ ->expects(self::never())
+ ->method('hasCalendarInCalendarHome');
+ $calendarPlugin1
+ ->expects(self::never())
+ ->method('getCalendarInCalendarHome');
+
+ $externalCalendarMock = $this->createMock(ExternalCalendar::class);
+
+ $calendarPlugin2 = $this->createMock(ICalendarProvider::class);
+ $calendarPlugin2
+ ->expects(self::once())
+ ->method('getAppId')
+ ->with()
+ ->willReturn('calendar_plugin_2');
+ $calendarPlugin2
+ ->expects(self::once())
+ ->method('hasCalendarInCalendarHome')
+ ->with('user-principal-123', 'calendar-uri-from-backend')
+ ->willReturn(true);
+ $calendarPlugin2
+ ->expects(self::once())
+ ->method('getCalendarInCalendarHome')
+ ->with('user-principal-123', 'calendar-uri-from-backend')
+ ->willReturn($externalCalendarMock);
+
+ $this->pluginManager
+ ->expects(self::once())
+ ->method('getCalendarPlugins')
+ ->with()
+ ->willReturn([$calendarPlugin1, $calendarPlugin2]);
+
+ $actual = $this->calendarHome->getChild('app-generated--calendar_plugin_2--calendar-uri-from-backend');
+ $this->assertEquals($externalCalendarMock, $actual);
+ }
+
+ public function testGetChildrenSubscriptions(): void {
+ $this->backend
+ ->expects(self::once())
+ ->method('getCalendarsForUser')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('user-principal-123')
+ ->willReturn([
+ [
+ 'id' => 'subscription-1',
+ 'uri' => 'subscription-1',
+ 'principaluri' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-1',
+ // A subscription array has actually more properties.
+ ],
+ [
+ 'id' => 'subscription-2',
+ 'uri' => 'subscription-2',
+ 'principaluri' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-2',
+ // A subscription array has actually more properties.
+ ]
+ ]);
+
+ /*
+ * @FIXME: PluginManager should be injected via constructor.
+ */
+
+ $pluginManager = $this->createMock(PluginManager::class);
+ $pluginManager
+ ->expects(self::once())
+ ->method('getCalendarPlugins')
+ ->with()
+ ->willReturn([]);
+
+ $calendarHome = new CalendarHome(
+ $this->backend,
+ $this->principalInfo,
+ $this->logger,
+ false
+ );
+
+ $reflection = new \ReflectionClass($calendarHome);
+ $reflectionProperty = $reflection->getProperty('pluginManager');
+ $reflectionProperty->setValue($calendarHome, $pluginManager);
+
+ $actual = $calendarHome->getChildren();
+
+ $this->assertCount(5, $actual);
+ $this->assertInstanceOf(Inbox::class, $actual[0]);
+ $this->assertInstanceOf(Outbox::class, $actual[1]);
+ $this->assertInstanceOf(TrashbinHome::class, $actual[2]);
+ $this->assertInstanceOf(Subscription::class, $actual[3]);
+ $this->assertInstanceOf(Subscription::class, $actual[4]);
+ }
+
+ public function testGetChildrenCachedSubscriptions(): void {
+ $this->backend
+ ->expects(self::once())
+ ->method('getCalendarsForUser')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('user-principal-123')
+ ->willReturn([
+ [
+ 'id' => 'subscription-1',
+ 'uri' => 'subscription-1',
+ 'principaluris' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-1',
+ // A subscription array has actually more properties.
+ ],
+ [
+ 'id' => 'subscription-2',
+ 'uri' => 'subscription-2',
+ 'principaluri' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-2',
+ // A subscription array has actually more properties.
+ ]
+ ]);
+
+ /*
+ * @FIXME: PluginManager should be injected via constructor.
+ */
+
+ $pluginManager = $this->createMock(PluginManager::class);
+ $pluginManager
+ ->expects(self::once())
+ ->method('getCalendarPlugins')
+ ->with()
+ ->willReturn([]);
+
+ $calendarHome = new CalendarHome(
+ $this->backend,
+ $this->principalInfo,
+ $this->logger,
+ true
+ );
+
+ $reflection = new \ReflectionClass($calendarHome);
+ $reflectionProperty = $reflection->getProperty('pluginManager');
+ $reflectionProperty->setValue($calendarHome, $pluginManager);
+
+ $actual = $calendarHome->getChildren();
+
+ $this->assertCount(5, $actual);
+ $this->assertInstanceOf(Inbox::class, $actual[0]);
+ $this->assertInstanceOf(Outbox::class, $actual[1]);
+ $this->assertInstanceOf(TrashbinHome::class, $actual[2]);
+ $this->assertInstanceOf(CachedSubscription::class, $actual[3]);
+ $this->assertInstanceOf(CachedSubscription::class, $actual[4]);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CalendarImplTest.php b/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
new file mode 100644
index 00000000000..d6a8f3b910e
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
@@ -0,0 +1,308 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use Generator;
+use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\CalendarImpl;
+use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
+use OCA\DAV\CalDAV\Schedule\Plugin;
+use OCA\DAV\Connector\Sabre\Server;
+use OCP\Calendar\Exceptions\CalendarException;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\ITip\Message;
+use Sabre\VObject\Reader;
+
+class CalendarImplTest extends \Test\TestCase {
+ private Calendar&MockObject $calendar;
+ private array $calendarInfo;
+ private CalDavBackend&MockObject $backend;
+ private CalendarImpl $calendarImpl;
+ private array $mockExportCollection;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calendar = $this->createMock(Calendar::class);
+ $this->calendarInfo = [
+ 'id' => 1,
+ '{DAV:}displayname' => 'user readable name 123',
+ '{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
+ 'uri' => '/this/is/a/uri',
+ 'principaluri' => 'principal/users/foobar'
+ ];
+ $this->backend = $this->createMock(CalDavBackend::class);
+
+ $this->calendarImpl = new CalendarImpl(
+ $this->calendar,
+ $this->calendarInfo,
+ $this->backend
+ );
+ }
+
+
+ public function testGetKey(): void {
+ $this->assertEquals($this->calendarImpl->getKey(), 1);
+ }
+
+ public function testGetDisplayname(): void {
+ $this->assertEquals($this->calendarImpl->getDisplayName(), 'user readable name 123');
+ }
+
+ public function testGetDisplayColor(): void {
+ $this->assertEquals($this->calendarImpl->getDisplayColor(), '#AABBCC');
+ }
+
+ public function testSearch(): void {
+ $this->backend->expects($this->once())
+ ->method('search')
+ ->with($this->calendarInfo, 'abc', ['def'], ['ghi'], 42, 1337)
+ ->willReturn(['SEARCHRESULTS']);
+
+ $result = $this->calendarImpl->search('abc', ['def'], ['ghi'], 42, 1337);
+ $this->assertEquals($result, ['SEARCHRESULTS']);
+ }
+
+ public function testGetPermissionRead(): void {
+ $this->calendar->expects($this->once())
+ ->method('getACL')
+ ->with()
+ ->willReturn([
+ ['privilege' => '{DAV:}read', 'principal' => 'principal/users/foobar'],
+ ['privilege' => '{DAV:}read', 'principal' => 'principal/users/other'],
+ ['privilege' => '{DAV:}write', 'principal' => 'principal/users/other'],
+ ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
+ ]);
+
+ $this->assertEquals(1, $this->calendarImpl->getPermissions());
+ }
+
+ public function testGetPermissionWrite(): void {
+ $this->calendar->expects($this->once())
+ ->method('getACL')
+ ->with()
+ ->willReturn([
+ ['privilege' => '{DAV:}write', 'principal' => 'principal/users/foobar'],
+ ['privilege' => '{DAV:}read', 'principal' => 'principal/users/other'],
+ ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
+ ]);
+
+ $this->assertEquals(6, $this->calendarImpl->getPermissions());
+ }
+
+ public function testGetPermissionReadWrite(): void {
+ $this->calendar->expects($this->once())
+ ->method('getACL')
+ ->with()
+ ->willReturn([
+ ['privilege' => '{DAV:}write', 'principal' => 'principal/users/foobar'],
+ ['privilege' => '{DAV:}read', 'principal' => 'principal/users/foobar'],
+ ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
+ ]);
+
+ $this->assertEquals(7, $this->calendarImpl->getPermissions());
+ }
+
+ public function testGetPermissionAll(): void {
+ $this->calendar->expects($this->once())
+ ->method('getACL')
+ ->with()
+ ->willReturn([
+ ['privilege' => '{DAV:}all', 'principal' => 'principal/users/foobar'],
+ ]);
+
+ $this->assertEquals(31, $this->calendarImpl->getPermissions());
+ }
+
+ public function testHandleImipMessage(): void {
+ $message = <<<EOF
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+VERSION:2.0
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com
+ORGANIZER:mailto:pierre@generalstore.com
+UID:aUniqueUid
+SEQUENCE:2
+REQUEST-STATUS:2.0;Success
+END:VEVENT
+END:VCALENDAR
+EOF;
+
+ /** @var CustomPrincipalPlugin|MockObject $authPlugin */
+ $authPlugin = $this->createMock(CustomPrincipalPlugin::class);
+ $authPlugin->expects(self::once())
+ ->method('setCurrentPrincipal')
+ ->with($this->calendar->getPrincipalURI());
+
+ /** @var \Sabre\DAVACL\Plugin|MockObject $aclPlugin */
+ $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
+
+ /** @var Plugin|MockObject $schedulingPlugin */
+ $schedulingPlugin = $this->createMock(Plugin::class);
+ $iTipMessage = $this->getITipMessage($message);
+ $iTipMessage->recipient = 'mailto:lewis@stardew-tent-living.com';
+
+ $server = $this->createMock(Server::class);
+ $server->expects($this->any())
+ ->method('getPlugin')
+ ->willReturnMap([
+ ['auth', $authPlugin],
+ ['acl', $aclPlugin],
+ ['caldav-schedule', $schedulingPlugin]
+ ]);
+ $server->expects(self::once())
+ ->method('emit');
+
+ $invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer', 'isExternalAttendee']);
+ $invitationResponseServer->server = $server;
+ $invitationResponseServer->expects($this->any())
+ ->method('getServer')
+ ->willReturn($server);
+ $invitationResponseServer->expects(self::once())
+ ->method('isExternalAttendee')
+ ->willReturn(false);
+
+ $calendarImpl = $this->getMockBuilder(CalendarImpl::class)
+ ->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
+ ->onlyMethods(['getInvitationResponseServer'])
+ ->getMock();
+ $calendarImpl->expects($this->once())
+ ->method('getInvitationResponseServer')
+ ->willReturn($invitationResponseServer);
+
+ $calendarImpl->handleIMipMessage('filename.ics', $message);
+ }
+
+ public function testHandleImipMessageNoCalendarUri(): void {
+ /** @var CustomPrincipalPlugin|MockObject $authPlugin */
+ $authPlugin = $this->createMock(CustomPrincipalPlugin::class);
+ $authPlugin->expects(self::once())
+ ->method('setCurrentPrincipal')
+ ->with($this->calendar->getPrincipalURI());
+ unset($this->calendarInfo['uri']);
+
+ /** @var Plugin|MockObject $schedulingPlugin */
+ $schedulingPlugin = $this->createMock(Plugin::class);
+
+ /** @var \Sabre\DAVACL\Plugin|MockObject $schedulingPlugin */
+ $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
+
+ $server
+ = $this->createMock(Server::class);
+ $server->expects($this->any())
+ ->method('getPlugin')
+ ->willReturnMap([
+ ['auth', $authPlugin],
+ ['acl', $aclPlugin],
+ ['caldav-schedule', $schedulingPlugin]
+ ]);
+ $server->expects(self::never())
+ ->method('emit');
+
+ $invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer']);
+ $invitationResponseServer->server = $server;
+ $invitationResponseServer->expects($this->any())
+ ->method('getServer')
+ ->willReturn($server);
+
+ $calendarImpl = $this->getMockBuilder(CalendarImpl::class)
+ ->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
+ ->onlyMethods(['getInvitationResponseServer'])
+ ->getMock();
+ $calendarImpl->expects($this->once())
+ ->method('getInvitationResponseServer')
+ ->willReturn($invitationResponseServer);
+
+ $message = <<<EOF
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+VERSION:2.0
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com
+ORGANIZER:mailto:pierre@generalstore.com
+UID:aUniqueUid
+SEQUENCE:2
+REQUEST-STATUS:2.0;Success
+END:VEVENT
+END:VCALENDAR
+EOF;
+
+ $this->expectException(CalendarException::class);
+ $calendarImpl->handleIMipMessage('filename.ics', $message);
+ }
+
+ private function getITipMessage($calendarData): Message {
+ $iTipMessage = new Message();
+ /** @var VCalendar $vObject */
+ $vObject = Reader::read($calendarData);
+ /** @var VEvent $vEvent */
+ $vEvent = $vObject->{'VEVENT'};
+ $orgaizer = $vEvent->{'ORGANIZER'}->getValue();
+ $attendee = $vEvent->{'ATTENDEE'}->getValue();
+
+ $iTipMessage->method = $vObject->{'METHOD'}->getValue();
+ $iTipMessage->recipient = $orgaizer;
+ $iTipMessage->sender = $attendee;
+ $iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
+ $iTipMessage->component = 'VEVENT';
+ $iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
+ $iTipMessage->message = $vObject;
+ return $iTipMessage;
+ }
+
+ protected function mockExportGenerator(): Generator {
+ foreach ($this->mockExportCollection as $entry) {
+ yield $entry;
+ }
+ }
+
+ public function testExport(): void {
+ // Arrange
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // construct data store return
+ $this->mockExportCollection[] = [
+ 'id' => 1,
+ 'calendardata' => $vCalendar->serialize()
+ ];
+ $this->backend->expects($this->once())
+ ->method('exportCalendar')
+ ->with(1, $this->backend::CALENDAR_TYPE_CALENDAR, null)
+ ->willReturn($this->mockExportGenerator());
+
+ // Act
+ foreach ($this->calendarImpl->export(null) as $entry) {
+ $exported[] = $entry;
+ }
+
+ // Assert
+ $this->assertCount(1, $exported, 'Invalid exported items count');
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/CalendarManagerTest.php b/apps/dav/tests/unit/CalDAV/CalendarManagerTest.php
new file mode 100644
index 00000000000..e8159ffe07c
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CalendarManagerTest.php
@@ -0,0 +1,64 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OC\Calendar\Manager;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\CalendarImpl;
+use OCA\DAV\CalDAV\CalendarManager;
+use OCP\Calendar\IManager;
+use OCP\IConfig;
+use OCP\IL10N;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+class CalendarManagerTest extends \Test\TestCase {
+ private CalDavBackend&MockObject $backend;
+ private IL10N&MockObject $l10n;
+ private IConfig&MockObject $config;
+ private LoggerInterface&MockObject $logger;
+ private CalendarManager $manager;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->backend = $this->createMock(CalDavBackend::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->manager = new CalendarManager(
+ $this->backend,
+ $this->l10n,
+ $this->config,
+ $this->logger
+ );
+ }
+
+ public function testSetupCalendarProvider(): void {
+ $this->backend->expects($this->once())
+ ->method('getCalendarsForUser')
+ ->with('principals/users/user123')
+ ->willReturn([
+ ['id' => 123, 'uri' => 'blablub1'],
+ ['id' => 456, 'uri' => 'blablub2'],
+ ]);
+
+ /** @var IManager&MockObject $calendarManager */
+ $calendarManager = $this->createMock(Manager::class);
+ $registeredIds = [];
+ $calendarManager->expects($this->exactly(2))
+ ->method('registerCalendar')
+ ->willReturnCallback(function ($parameter) use (&$registeredIds): void {
+ $this->assertInstanceOf(CalendarImpl::class, $parameter);
+ $registeredIds[] = $parameter->getKey();
+ });
+
+ $this->manager->setupCalendarProvider($calendarManager, 'user123');
+
+ $this->assertEquals(['123','456'], $registeredIds);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CalendarTest.php b/apps/dav/tests/unit/CalDAV/CalendarTest.php
new file mode 100644
index 00000000000..b0d3c35bfe7
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CalendarTest.php
@@ -0,0 +1,608 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\BirthdayService;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Calendar;
+use OCP\IConfig;
+use OCP\IL10N;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\PropPatch;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+class CalendarTest extends TestCase {
+ protected IL10N&MockObject $l10n;
+ protected IConfig&MockObject $config;
+ protected LoggerInterface&MockObject $logger;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->l10n
+ ->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($text, $parameters = []) {
+ return vsprintf($text, $parameters);
+ });
+ }
+
+ public function testDelete(): void {
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $backend->expects($this->never())
+ ->method('updateShares');
+ $backend->expects($this->once())
+ ->method('unshare');
+
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
+ $c->delete();
+ }
+
+
+ public function testDeleteFromGroup(): void {
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $backend->expects($this->never())
+ ->method('updateShares');
+ $backend->expects($this->once())
+ ->method('unshare');
+
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
+ $c->delete();
+ }
+
+ public function testDeleteOwn(): void {
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $backend->expects($this->never())->method('updateShares');
+ $backend->expects($this->never())->method('getShares');
+
+ $this->config->expects($this->never())->method('setUserValue');
+
+ $backend->expects($this->once())->method('deleteCalendar')
+ ->with(666);
+
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user1',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
+ $c->delete();
+ }
+
+ public function testDeleteBirthdayCalendar(): void {
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $backend->expects($this->once())->method('deleteCalendar')
+ ->with(666);
+
+ $this->config->expects($this->once())
+ ->method('setUserValue')
+ ->with('user1', 'dav', 'generateBirthdayCalendar', 'no');
+
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'principals/users/user1',
+ 'principaluri' => 'principals/users/user1',
+ 'id' => 666,
+ 'uri' => 'contact_birthdays',
+ '{DAV:}displayname' => 'Test',
+ ];
+
+ $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
+ $c->delete();
+ }
+
+ public static function dataPropPatch(): array {
+ return [
+ ['user1', 'user2', [], true],
+ ['user1', 'user2', [
+ '{http://owncloud.org/ns}calendar-enabled' => true,
+ ], true],
+ ['user1', 'user2', [
+ '{DAV:}displayname' => true,
+ ], true],
+ ['user1', 'user2', [
+ '{DAV:}displayname' => true,
+ '{http://owncloud.org/ns}calendar-enabled' => true,
+ ], true],
+ ['user1', 'user1', [], false],
+ ['user1', 'user1', [
+ '{http://owncloud.org/ns}calendar-enabled' => true,
+ ], false],
+ ['user1', 'user1', [
+ '{DAV:}displayname' => true,
+ ], false],
+ ['user1', 'user1', [
+ '{DAV:}displayname' => true,
+ '{http://owncloud.org/ns}calendar-enabled' => true,
+ ], false],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataPropPatch')]
+ public function testPropPatch(string $ownerPrincipal, string $principalUri, array $mutations, bool $shared): void {
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => $ownerPrincipal,
+ 'principaluri' => $principalUri,
+ 'id' => 666,
+ 'uri' => 'default'
+ ];
+ $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
+ $propPatch = new PropPatch($mutations);
+
+ if (!$shared) {
+ $backend->expects($this->once())
+ ->method('updateCalendar')
+ ->with(666, $propPatch);
+ }
+ $c->propPatch($propPatch);
+ $this->addToAssertionCount(1);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesReadOnlyInfo')]
+ public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet, $uri = 'default'): void {
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1);
+ $calendarInfo = [
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => $uri
+ ];
+ $calendarInfo['{DAV:}displayname'] = 'Test';
+ if (!is_null($readOnlyValue)) {
+ $calendarInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue;
+ }
+ if ($hasOwnerSet) {
+ $calendarInfo['{http://owncloud.org/ns}owner-principal'] = 'user1';
+ }
+ $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
+ $acl = $c->getACL();
+ $childAcl = $c->getChildACL();
+
+ $expectedAcl = [[
+ 'privilege' => '{DAV:}read',
+ 'principal' => $hasOwnerSet ? 'user1' : 'user2',
+ 'protected' => true
+ ], [
+ 'privilege' => '{DAV:}read',
+ 'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-write',
+ 'protected' => true,
+ ], [
+ 'privilege' => '{DAV:}read',
+ 'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-read',
+ 'protected' => true,
+ ]];
+ if ($uri === BirthdayService::BIRTHDAY_CALENDAR_URI) {
+ $expectedAcl[] = [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $hasOwnerSet ? 'user1' : 'user2',
+ 'protected' => true
+ ];
+ $expectedAcl[] = [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-write',
+ 'protected' => true
+ ];
+ } else {
+ $expectedAcl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $hasOwnerSet ? 'user1' : 'user2',
+ 'protected' => true
+ ];
+ $expectedAcl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-write',
+ 'protected' => true
+ ];
+ }
+
+ $expectedAcl[] = [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-read',
+ 'protected' => true
+ ];
+
+ if ($hasOwnerSet) {
+ $expectedAcl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user2',
+ 'protected' => true
+ ];
+ if ($expectsWrite) {
+ $expectedAcl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => 'user2',
+ 'protected' => true
+ ];
+ } else {
+ $expectedAcl[] = [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => 'user2',
+ 'protected' => true
+ ];
+ }
+ }
+ $this->assertEquals($expectedAcl, $acl);
+ $this->assertEquals($expectedAcl, $childAcl);
+ }
+
+ public static function providesReadOnlyInfo(): array {
+ return [
+ 'read-only property not set' => [true, null, true],
+ 'read-only property is false' => [true, false, true],
+ 'read-only property is true' => [false, true, true],
+ 'read-only property not set and no owner' => [true, null, false],
+ 'read-only property is false and no owner' => [true, false, false],
+ 'read-only property is true and no owner' => [false, true, false],
+ 'birthday calendar' => [false, false, false, BirthdayService::BIRTHDAY_CALENDAR_URI]
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')]
+ public function testPrivateClassification(int $expectedChildren, bool $isShared): void {
+ $calObject0 = ['uri' => 'event-0', 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC];
+ $calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL];
+ $calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE];
+
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $backend->expects($this->any())->method('getCalendarObjects')->willReturn([
+ $calObject0, $calObject1, $calObject2
+ ]);
+ $backend->expects($this->any())->method('getMultipleCalendarObjects')
+ ->with(666, ['event-0', 'event-1', 'event-2'])
+ ->willReturn([
+ $calObject0, $calObject1, $calObject2
+ ]);
+ $backend->expects($this->any())->method('getCalendarObject')
+ ->willReturn($calObject2)->with(666, 'event-2');
+ $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1);
+
+ $calendarInfo = [
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ if ($isShared) {
+ $calendarInfo['{http://owncloud.org/ns}owner-principal'] = 'user1';
+ }
+ $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
+ $children = $c->getChildren();
+ $this->assertEquals($expectedChildren, count($children));
+ $children = $c->getMultipleChildren(['event-0', 'event-1', 'event-2']);
+ $this->assertEquals($expectedChildren, count($children));
+
+ $this->assertEquals(!$isShared, $c->childExists('event-2'));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')]
+ public function testConfidentialClassification(int $expectedChildren, bool $isShared): void {
+ $start = '20160609';
+ $end = '20160610';
+
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//ownCloud calendar v1.2.2
+BEGIN:VEVENT
+CREATED:20160602T133732
+DTSTAMP:20160602T133732
+LAST-MODIFIED:20160602T133732
+UID:wej2z68l9h
+SUMMARY:Test Event
+LOCATION:Somewhere ...
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL;CN=de
+ epdiver:MAILTO:thomas.mueller@tmit.eu
+ORGANIZER;CN=deepdiver:MAILTO:thomas.mueller@tmit.eu
+DESCRIPTION:maybe ....
+DTSTART;TZID=Europe/Berlin;VALUE=DATE:$start
+DTEND;TZID=Europe/Berlin;VALUE=DATE:$end
+RRULE:FREQ=DAILY
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+DTSTART:19810329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+TZNAME:MESZ
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19961027T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+TZNAME:MEZ
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+EOD;
+
+ $calObject0 = ['uri' => 'event-0', 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC];
+ $calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL, 'calendardata' => $calData];
+ $calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE];
+
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $backend->expects($this->any())->method('getCalendarObjects')->willReturn([
+ $calObject0, $calObject1, $calObject2
+ ]);
+ $backend->expects($this->any())->method('getMultipleCalendarObjects')
+ ->with(666, ['event-0', 'event-1', 'event-2'])
+ ->willReturn([
+ $calObject0, $calObject1, $calObject2
+ ]);
+ $backend->expects($this->any())->method('getCalendarObject')
+ ->willReturn($calObject1)->with(666, 'event-1');
+ $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1);
+
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => $isShared ? 'user1' : 'user2',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
+
+ $this->assertEquals(count($c->getChildren()), $expectedChildren);
+
+ // test private event
+ $privateEvent = $c->getChild('event-1');
+ $calData = $privateEvent->get();
+ $event = Reader::read($calData);
+
+ $this->assertEquals($start, $event->VEVENT->DTSTART->getValue());
+ $this->assertEquals($end, $event->VEVENT->DTEND->getValue());
+
+ if ($isShared) {
+ $this->assertEquals('Busy', $event->VEVENT->SUMMARY->getValue());
+ $this->assertArrayNotHasKey('ATTENDEE', $event->VEVENT);
+ $this->assertArrayNotHasKey('LOCATION', $event->VEVENT);
+ $this->assertArrayNotHasKey('DESCRIPTION', $event->VEVENT);
+ $this->assertArrayNotHasKey('ORGANIZER', $event->VEVENT);
+ } else {
+ $this->assertEquals('Test Event', $event->VEVENT->SUMMARY->getValue());
+ }
+
+ // Test l10n
+ $l10n = $this->createMock(IL10N::class);
+ if ($isShared) {
+ $l10n->expects($this->once())
+ ->method('t')
+ ->with('Busy')
+ ->willReturn('Translated busy');
+ } else {
+ $l10n->expects($this->never())
+ ->method('t');
+ }
+ $c = new Calendar($backend, $calendarInfo, $l10n, $this->config, $this->logger);
+
+ $calData = $c->getChild('event-1')->get();
+ $event = Reader::read($calData);
+
+ if ($isShared) {
+ $this->assertEquals('Translated busy', $event->VEVENT->SUMMARY->getValue());
+ } else {
+ $this->assertEquals('Test Event', $event->VEVENT->SUMMARY->getValue());
+ }
+ }
+
+ public static function providesConfidentialClassificationData(): array {
+ return [
+ [3, false],
+ [2, true]
+ ];
+ }
+
+ public function testRemoveVAlarms(): void {
+ $publicObjectData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Nextcloud calendar v1.5.6
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20171022T125130
+DTSTAMP:20171022T125130
+LAST-MODIFIED:20171022T125130
+UID:PPL24TH8UGOWE94XET87ER
+SUMMARY:Foo bar blub
+CLASS:PUBLIC
+STATUS:CONFIRMED
+DTSTART;VALUE=DATE:20171024
+DTEND;VALUE=DATE:20171025
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+EOD;
+
+ $confidentialObjectData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Nextcloud calendar v1.5.6
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20171022T125130
+DTSTAMP:20171022T125130
+LAST-MODIFIED:20171022T125130
+UID:PPL24TH8UGOWE94XET87ER
+SUMMARY:Foo bar blub
+CLASS:CONFIDENTIAL
+STATUS:CONFIRMED
+DTSTART;VALUE=DATE:20171024
+DTEND;VALUE=DATE:20171025
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+EOD;
+
+ $publicObjectDataWithoutVAlarm = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Nextcloud calendar v1.5.6
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20171022T125130
+DTSTAMP:20171022T125130
+LAST-MODIFIED:20171022T125130
+UID:PPL24TH8UGOWE94XET87ER
+SUMMARY:Foo bar blub
+CLASS:PUBLIC
+STATUS:CONFIRMED
+DTSTART;VALUE=DATE:20171024
+DTEND;VALUE=DATE:20171025
+END:VEVENT
+END:VCALENDAR
+
+EOD;
+
+ $confidentialObjectCleaned = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Nextcloud calendar v1.5.6
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20171022T125130
+UID:PPL24TH8UGOWE94XET87ER
+SUMMARY:Busy
+CLASS:CONFIDENTIAL
+DTSTART;VALUE=DATE:20171024
+DTEND;VALUE=DATE:20171025
+END:VEVENT
+END:VCALENDAR
+
+EOD;
+
+
+
+ $publicObject = ['uri' => 'event-0',
+ 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC,
+ 'calendardata' => $publicObjectData];
+
+ $confidentialObject = ['uri' => 'event-1',
+ 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL,
+ 'calendardata' => $confidentialObjectData];
+
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $backend->expects($this->any())
+ ->method('getCalendarObjects')
+ ->willReturn([$publicObject, $confidentialObject]);
+
+ $backend->expects($this->any())
+ ->method('getMultipleCalendarObjects')
+ ->with(666, ['event-0', 'event-1'])
+ ->willReturn([$publicObject, $confidentialObject]);
+
+ $backend->expects($this->any())
+ ->method('getCalendarObject')
+ ->willReturnCallback(function ($cId, $uri) use ($publicObject, $confidentialObject) {
+ switch ($uri) {
+ case 'event-0':
+ return $publicObject;
+
+ case 'event-1':
+ return $confidentialObject;
+
+ default:
+ throw new \Exception('unexpected uri');
+ }
+ });
+
+ $backend->expects($this->any())
+ ->method('applyShareAcl')
+ ->willReturnArgument(1);
+
+ $calendarInfoOwner = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user1',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ $calendarInfoSharedRW = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ $calendarInfoSharedRO = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ '{http://owncloud.org/ns}read-only' => true,
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+
+ $ownerCalendar = new Calendar($backend, $calendarInfoOwner, $this->l10n, $this->config, $this->logger);
+ $rwCalendar = new Calendar($backend, $calendarInfoSharedRW, $this->l10n, $this->config, $this->logger);
+ $roCalendar = new Calendar($backend, $calendarInfoSharedRO, $this->l10n, $this->config, $this->logger);
+
+ $this->assertCount(2, $ownerCalendar->getChildren());
+ $this->assertCount(2, $rwCalendar->getChildren());
+ $this->assertCount(2, $roCalendar->getChildren());
+
+ // calendar data shall not be altered for the owner
+ $this->assertEquals($ownerCalendar->getChild('event-0')->get(), $publicObjectData);
+ $this->assertEquals($ownerCalendar->getChild('event-1')->get(), $confidentialObjectData);
+
+ // valarms shall not be removed for read-write shares
+ $this->assertEquals(
+ $this->fixLinebreak($rwCalendar->getChild('event-0')->get()),
+ $this->fixLinebreak($publicObjectData));
+ $this->assertEquals(
+ $this->fixLinebreak($rwCalendar->getChild('event-1')->get()),
+ $this->fixLinebreak($confidentialObjectCleaned));
+
+ // valarms shall be removed for read-only shares
+ $this->assertEquals(
+ $this->fixLinebreak($roCalendar->getChild('event-0')->get()),
+ $this->fixLinebreak($publicObjectDataWithoutVAlarm));
+ $this->assertEquals(
+ $this->fixLinebreak($roCalendar->getChild('event-1')->get()),
+ $this->fixLinebreak($confidentialObjectCleaned));
+ }
+
+ private function fixLinebreak(string $str): string {
+ return preg_replace('~(*BSR_ANYCRLF)\R~', "\r\n", $str);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php b/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php
new file mode 100644
index 00000000000..194009827da
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php
@@ -0,0 +1,171 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
+use Test\TestCase;
+
+class DefaultCalendarValidatorTest extends TestCase {
+ private DefaultCalendarValidator $validator;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->validator = new DefaultCalendarValidator();
+ }
+
+ public function testValidateScheduleDefaultCalendar(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(true);
+ $node->expects(self::once())
+ ->method('isShared')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('isDeleted')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('getProperties')
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT']),
+ ]);
+
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithEmptyProperties(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(true);
+ $node->expects(self::once())
+ ->method('isShared')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('isDeleted')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('getProperties')
+ ->willReturn([]);
+
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithSubscription(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(true);
+ $node->expects(self::never())
+ ->method('canWrite');
+ $node->expects(self::never())
+ ->method('isShared');
+ $node->expects(self::never())
+ ->method('isDeleted');
+ $node->expects(self::never())
+ ->method('getProperties');
+
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithoutWrite(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(false);
+ $node->expects(self::never())
+ ->method('isShared');
+ $node->expects(self::never())
+ ->method('isDeleted');
+ $node->expects(self::never())
+ ->method('getProperties');
+
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithShared(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(true);
+ $node->expects(self::once())
+ ->method('isShared')
+ ->willReturn(true);
+ $node->expects(self::never())
+ ->method('isDeleted');
+ $node->expects(self::never())
+ ->method('getProperties');
+
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithDeleted(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(true);
+ $node->expects(self::once())
+ ->method('isShared')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('isDeleted')
+ ->willReturn(true);
+ $node->expects(self::never())
+ ->method('getProperties');
+
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithoutVeventSupport(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(true);
+ $node->expects(self::once())
+ ->method('isShared')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('isDeleted')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('getProperties')
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO']),
+ ]);
+
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php b/apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php
new file mode 100644
index 00000000000..90b6f9ec0db
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php
@@ -0,0 +1,188 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\EventComparisonService;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+class EventComparisonServiceTest extends TestCase {
+ private EventComparisonService $eventComparisonService;
+
+ protected function setUp(): void {
+ $this->eventComparisonService = new EventComparisonService();
+ }
+
+ public function testNoModifiedEvent(): void {
+ $vCalendarOld = new VCalendar();
+ $vCalendarNew = new VCalendar();
+
+ $vEventOld = $vCalendarOld->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventOld->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventOld->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventNew = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld);
+ $this->assertEmpty($result['old']);
+ $this->assertEmpty($result['new']);
+ }
+
+ public function testNewEvent(): void {
+ $vCalendarOld = null;
+ $vCalendarNew = new VCalendar();
+
+ $vEventNew = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld);
+ $this->assertNull($result['old']);
+ $this->assertEquals([$vEventNew], $result['new']);
+ }
+
+ public function testModifiedUnmodifiedEvent(): void {
+ $vCalendarOld = new VCalendar();
+ $vCalendarNew = new VCalendar();
+
+ $vEventOld1 = $vCalendarOld->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+ $vEventOld1->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventOld1->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventOld2 = $vCalendarOld->add('VEVENT', [
+ 'UID' => 'uid-1235',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+ $vEventOld2->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventOld2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventNew1 = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+ $vEventNew1->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew1->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventNew2 = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1235',
+ 'LAST-MODIFIED' => 123457,
+ 'SEQUENCE' => 3,
+ 'SUMMARY' => 'Fellowship meeting 2',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+ $vEventNew2->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld);
+ $this->assertEquals([$vEventOld2], $result['old']);
+ $this->assertEquals([$vEventNew2], $result['new']);
+ }
+
+ // First test to certify fix for issue nextcloud/server#41084
+ public function testSequenceNumberIncrementDetectedForFirstModificationToEventWithoutZeroInit(): void {
+ $vCalendarOld = new VCalendar();
+ $vCalendarNew = new VCalendar();
+
+ $vEventOld = $vCalendarOld->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ // 'SEQUENCE' => 0, // sequence number may not be set to zero during event creation and instead fully omitted
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventOld->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventOld->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventNew = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld);
+ $this->assertEquals([$vEventOld], $result['old']);
+ $this->assertEquals([$vEventNew], $result['new']);
+ }
+
+ // Second test to certify fix for issue nextcloud/server#41084
+ public function testSequenceNumberIncrementDetectedForFirstModificationToEventWithZeroInit(): void {
+ $vCalendarOld = new VCalendar();
+ $vCalendarNew = new VCalendar();
+
+ $vEventOld = $vCalendarOld->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventOld->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventOld->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventNew = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld);
+ $this->assertEquals([$vEventOld], $result['old']);
+ $this->assertEquals([$vEventNew], $result['new']);
+ }
+
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/EventReaderTest.php b/apps/dav/tests/unit/CalDAV/EventReaderTest.php
new file mode 100644
index 00000000000..3bd4f9d85c2
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/EventReaderTest.php
@@ -0,0 +1,1087 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use DateTimeZone;
+use OCA\DAV\CalDAV\EventReader;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+class EventReaderTest extends TestCase {
+
+ private VCalendar $vCalendar1a;
+ private VCalendar $vCalendar1b;
+ private VCalendar $vCalendar1c;
+ private VCalendar $vCalendar1d;
+ private VCalendar $vCalendar1e;
+ private VCalendar $vCalendar2;
+ private VCalendar $vCalendar3;
+
+ protected function setUp(): void {
+
+ parent::setUp();
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $this->vCalendar1a = new VCalendar();
+ $vEvent = $this->vCalendar1a->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and different start/end time zones
+ $this->vCalendar1b = new VCalendar();
+ $vEvent = $this->vCalendar1b->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Vancouver']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and global time zone
+ $this->vCalendar1c = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar1c->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar1c->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000');
+ $vEvent->add('DTEND', '20240701T090000');
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and no time zone
+ $this->vCalendar1d = new VCalendar();
+ $vEvent = $this->vCalendar1d->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000');
+ $vEvent->add('DTEND', '20240701T090000');
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and Microsoft time zone
+ $this->vCalendar1e = new VCalendar();
+ $vEvent = $this->vCalendar1e->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'Eastern Standard Time']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'Eastern Standard Time']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a full day event
+ $this->vCalendar2 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar2->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar2->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240702');
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a multi day event
+ $this->vCalendar3 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar3->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar3->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240706');
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ }
+
+ public function testConstructFromCalendarString(): void {
+
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a->serialize(), '96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ // test object creation
+ $this->assertInstanceOf(EventReader::class, $er);
+
+ }
+
+ public function testConstructFromCalendarObject(): void {
+
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, '96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ // test object creation
+ $this->assertInstanceOf(EventReader::class, $er);
+
+ }
+
+ public function testConstructFromEventObject(): void {
+
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a->VEVENT[0]);
+ // test object creation
+ $this->assertInstanceOf(EventReader::class, $er);
+
+ }
+
+ public function testStartDateTime(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('UTC')))), $er->startDateTime());
+
+ /** test day part event with microsoft time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T000000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T000000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ }
+
+ public function testStartTimeZone(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('UTC')), $er->startTimeZone());
+
+ /** test day part event with microsoft time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ }
+
+ public function testEndDate(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Vancouver')))), $er->endDateTime());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('UTC')))), $er->endDateTime());
+
+ /** test day part event with microsoft time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240702T000000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240706T000000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ }
+
+ public function testEndTimeZone(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Vancouver')), $er->endTimeZone());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('UTC')), $er->endTimeZone());
+
+ /** test day part event with microsoft time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ }
+
+ public function testEntireDay(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->entireDay());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->entireDay());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->entireDay());
+
+ }
+
+ public function testRecurs(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->recurs());
+
+ /** test rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurs());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurs());
+
+ }
+
+ public function testRecurringPattern(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringPattern());
+
+ /** test absolute rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('A', $er->recurringPattern());
+
+ /** test relative rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('R', $er->recurringPattern());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('A', $er->recurringPattern());
+
+ }
+
+ public function testRecurringPrecision(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringPrecision());
+
+ /** test daily rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('daily', $er->recurringPrecision());
+
+ /** test weekly rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('weekly', $er->recurringPrecision());
+
+ /** test monthly rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8,15');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('monthly', $er->recurringPrecision());
+
+ /** test yearly rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('yearly', $er->recurringPrecision());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('fixed', $er->recurringPrecision());
+
+ }
+
+ public function testRecurringInterval(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringInterval());
+
+ /** test daily rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(2, $er->recurringInterval());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringInterval());
+
+ }
+
+ public function testRecurringConcludes(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->recurringConcludes());
+
+ /** test rrule recurrance with no end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->recurringConcludes());
+
+ /** test rrule recurrance with until date end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rrule recurrance with iteration end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rdate (multiple property instances) recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rrule and rdate recurrance with rdate as last date */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rrule and rdate recurrance with rrule as last date */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=7;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240713');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ }
+
+ public function testRecurringConcludesAfter(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringConcludesAfter());
+
+ /** test rrule recurrance with count */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(6, $er->recurringConcludesAfter());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(2, $er->recurringConcludesAfter());
+
+ /** test rdate (multiple property instances) recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(2, $er->recurringConcludesAfter());
+
+ /** test rrule and rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(8, $er->recurringConcludesAfter());
+
+ }
+
+ public function testRecurringConcludesOn(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringConcludesOn());
+
+ /** test rrule recurrance with no end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringConcludesOn());
+
+ /** test rrule recurrance with until date end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+
+ // TODO: Fix until time zone
+ //$this->assertEquals((new \DateTime('20240712T080000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240705T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ /** test rdate (multiple property instances) recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240705T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ /** test rrule and rdate recurrance with rdate as last date */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240715T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ /** test rrule and rdate recurrance with rrule as last date */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=7;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240713');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240715T080000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ }
+
+ public function testRecurringDaysOfWeek(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfWeek());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['MO','WE','FR'], $er->recurringDaysOfWeek());
+
+ }
+
+ public function testRecurringDaysOfWeekNamed(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfWeekNamed());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['Monday','Wednesday','Friday'], $er->recurringDaysOfWeekNamed());
+
+ }
+
+ public function testRecurringDaysOfMonth(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfMonth());
+
+ /** test rrule recurrance with monthly absolute dates*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=6,13,20,27');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([6,13,20,27], $er->recurringDaysOfMonth());
+
+ }
+
+ public function testRecurringDaysOfYear(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfYear());
+
+ /** test rrule recurrance with monthly absolute dates*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYYEARDAY=1,30,180,365');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([1,30,180,365], $er->recurringDaysOfYear());
+
+ }
+
+ public function testRecurringWeeksOfMonth(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringWeeksOfMonth());
+
+ /** test rrule recurrance with monthly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([1], $er->recurringWeeksOfMonth());
+
+ }
+
+ public function testRecurringWeeksOfMonthNamed(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringWeeksOfMonthNamed());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['First'], $er->recurringWeeksOfMonthNamed());
+
+ }
+
+ public function testRecurringWeeksOfYear(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringWeeksOfYear());
+
+ /** test rrule recurrance with monthly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYWEEKNO=35,42;BYDAY=TU');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([35,42], $er->recurringWeeksOfYear());
+
+ }
+
+ public function testRecurringMonthsOfYear(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringMonthsOfYear());
+
+ /** test rrule recurrance with monthly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYMONTH=7;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([7], $er->recurringMonthsOfYear());
+
+ }
+
+ public function testRecurringMonthsOfYearNamed(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringMonthsOfYearNamed());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYMONTH=7;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['July'], $er->recurringMonthsOfYearNamed());
+
+ }
+
+ public function testRecurringIterationDaily(): void {
+
+ /** test rrule recurrance with daily frequency*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240714T040000Z');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240704T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240707T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240713T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240709T080000')));
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationWeekly(): void {
+
+ /** test rrule recurrance with weekly frequency*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240713T040000Z');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240703T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240705T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240708T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240712T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240709T080000')));
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationMonthlyAbsolute(): void {
+
+ /** test rrule recurrance with monthly absolute frequency on the 1st of each month*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;COUNT=3;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240801T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240901T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240809T080000')));
+ $this->assertEquals((new \DateTime('20240901T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationMonthlyRelative(): void {
+
+ /** test rrule recurrance with monthly relative frequency on the first monday of each month*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;COUNT=3;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240805T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240902T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240809T080000')));
+ $this->assertEquals((new \DateTime('20240902T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationYearlyAbsolute(): void {
+
+ /** test rrule recurrance with yearly absolute frequency on the 1st of july*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;COUNT=3;BYMONTH=7');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20250701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20260701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20250809T080000')));
+ $this->assertEquals((new \DateTime('20260701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationYearlyRelative(): void {
+
+ /** test rrule recurrance with yearly relative frequency on the first monday of july*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;COUNT=3;BYMONTH=7;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20250707T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20260706T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20250809T080000')));
+ $this->assertEquals((new \DateTime('20260706T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationFixed(): void {
+
+ /** test rrule recurrance with yearly relative frequency on the first monday of july*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240905T080000,20241231T080000');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240703T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240905T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20241231T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240809T080000')));
+ $this->assertEquals((new \DateTime('20240905T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php b/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php
new file mode 100644
index 00000000000..838dfc18f2f
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Export;
+
+use Generator;
+use OCA\DAV\CalDAV\Export\ExportService;
+use OCP\Calendar\CalendarExportOptions;
+use OCP\Calendar\ICalendarExport;
+use OCP\ServerVersion;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+
+class ExportServiceTest extends \Test\TestCase {
+ private ServerVersion&MockObject $serverVersion;
+ private ExportService $service;
+ private ICalendarExport&MockObject $calendar;
+ private array $mockExportCollection;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->serverVersion = $this->createMock(ServerVersion::class);
+ $this->serverVersion->method('getVersionString')
+ ->willReturn('32.0.0.0');
+ $this->service = new ExportService($this->serverVersion);
+ $this->calendar = $this->createMock(ICalendarExport::class);
+
+ }
+
+ protected function mockGenerator(): Generator {
+ foreach ($this->mockExportCollection as $entry) {
+ yield $entry;
+ }
+ }
+
+ public function testExport(): void {
+ // Arrange
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ /** @var \Sabre\VObject\Component\VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // construct calendar return
+ $options = new CalendarExportOptions();
+ $this->mockExportCollection[] = $vCalendar;
+ $this->calendar->expects($this->once())
+ ->method('export')
+ ->with($options)
+ ->willReturn($this->mockGenerator());
+
+ // Act
+ $document = '';
+ foreach ($this->service->export($this->calendar, $options) as $chunk) {
+ $document .= $chunk;
+ }
+
+ // Assert
+ $this->assertStringContainsString('BEGIN:VCALENDAR', $document, 'Exported document calendar start missing');
+ $this->assertStringContainsString('BEGIN:VEVENT', $document, 'Exported document event start missing');
+ $this->assertStringContainsString('END:VEVENT', $document, 'Exported document event end missing');
+ $this->assertStringContainsString('END:VCALENDAR', $document, 'Exported document calendar end missing');
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php b/apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php
new file mode 100644
index 00000000000..b2f479ac0e3
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php
@@ -0,0 +1,101 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Integration;
+
+use OCA\DAV\CalDAV\Integration\ExternalCalendar;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class ExternalCalendarTest extends TestCase {
+ private ExternalCalendar&MockObject $abstractExternalCalendar;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->abstractExternalCalendar
+ = $this->getMockForAbstractClass(ExternalCalendar::class, ['example-app-id', 'calendar-uri-in-backend']);
+ }
+
+ public function testGetName():void {
+ // Check that the correct name is returned
+ $this->assertEquals('app-generated--example-app-id--calendar-uri-in-backend',
+ $this->abstractExternalCalendar->getName());
+
+ // Check that the method is final and can't be overridden by other classes
+ $reflectionMethod = new \ReflectionMethod(ExternalCalendar::class, 'getName');
+ $this->assertTrue($reflectionMethod->isFinal());
+ }
+
+ public function testSetName():void {
+ // Check that the method is final and can't be overridden by other classes
+ $reflectionMethod = new \ReflectionMethod(ExternalCalendar::class, 'setName');
+ $this->assertTrue($reflectionMethod->isFinal());
+
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+ $this->expectExceptionMessage('Renaming calendars is not yet supported');
+
+ $this->abstractExternalCalendar->setName('other-name');
+ }
+
+ public function createDirectory(): void {
+ // Check that the method is final and can't be overridden by other classes
+ $reflectionMethod = new \ReflectionMethod(ExternalCalendar::class, 'createDirectory');
+ $this->assertTrue($reflectionMethod->isFinal());
+
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+ $this->expectExceptionMessage('Creating collections in calendar objects is not allowed');
+
+ $this->abstractExternalCalendar->createDirectory('other-name');
+ }
+
+ public function testIsAppGeneratedCalendar():void {
+ $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('personal'));
+ $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('work'));
+ $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('contact_birthdays'));
+ $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('company'));
+ $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('app-generated'));
+ $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('app-generated--example'));
+
+ $this->assertTrue(ExternalCalendar::isAppGeneratedCalendar('app-generated--deck--board-1'));
+ $this->assertTrue(ExternalCalendar::isAppGeneratedCalendar('app-generated--example--foo-2'));
+ $this->assertTrue(ExternalCalendar::isAppGeneratedCalendar('app-generated--example--foo--2'));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('splitAppGeneratedCalendarUriDataProvider')]
+ public function testSplitAppGeneratedCalendarUriInvalid(string $name):void {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Provided calendar uri was not app-generated');
+
+ ExternalCalendar::splitAppGeneratedCalendarUri($name);
+ }
+
+ public static function splitAppGeneratedCalendarUriDataProvider():array {
+ return [
+ ['personal'],
+ ['foo_shared_by_admin'],
+ ['contact_birthdays'],
+ ];
+ }
+
+ public function testSplitAppGeneratedCalendarUri():void {
+ $this->assertEquals(['deck', 'board-1'], ExternalCalendar::splitAppGeneratedCalendarUri('app-generated--deck--board-1'));
+ $this->assertEquals(['example', 'foo-2'], ExternalCalendar::splitAppGeneratedCalendarUri('app-generated--example--foo-2'));
+ $this->assertEquals(['example', 'foo--2'], ExternalCalendar::splitAppGeneratedCalendarUri('app-generated--example--foo--2'));
+ }
+
+ public function testDoesViolateReservedName():void {
+ $this->assertFalse(ExternalCalendar::doesViolateReservedName('personal'));
+ $this->assertFalse(ExternalCalendar::doesViolateReservedName('work'));
+ $this->assertFalse(ExternalCalendar::doesViolateReservedName('contact_birthdays'));
+ $this->assertFalse(ExternalCalendar::doesViolateReservedName('company'));
+
+ $this->assertTrue(ExternalCalendar::doesViolateReservedName('app-generated'));
+ $this->assertTrue(ExternalCalendar::doesViolateReservedName('app-generated-calendar'));
+ $this->assertTrue(ExternalCalendar::doesViolateReservedName('app-generated--deck-123'));
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php b/apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php
new file mode 100644
index 00000000000..3ba0b832593
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Listeners;
+
+use OCA\DAV\CalDAV\Activity\Backend;
+use OCA\DAV\Events\CalendarPublishedEvent;
+use OCA\DAV\Events\CalendarUnpublishedEvent;
+use OCA\DAV\Listener\CalendarPublicationListener;
+use OCP\EventDispatcher\Event;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class CalendarPublicationListenerTest extends TestCase {
+ private Backend&MockObject $activityBackend;
+ private LoggerInterface&MockObject $logger;
+ private CalendarPublicationListener $calendarPublicationListener;
+ private CalendarPublishedEvent&MockObject $publicationEvent;
+ private CalendarUnpublishedEvent&MockObject $unpublicationEvent;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->activityBackend = $this->createMock(Backend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->publicationEvent = $this->createMock(CalendarPublishedEvent::class);
+ $this->unpublicationEvent = $this->createMock(CalendarUnpublishedEvent::class);
+ $this->calendarPublicationListener = new CalendarPublicationListener($this->activityBackend, $this->logger);
+ }
+
+ public function testInvalidEvent(): void {
+ $this->activityBackend->expects($this->never())->method('onCalendarPublication');
+ $this->logger->expects($this->never())->method('debug');
+ $this->calendarPublicationListener->handle(new Event());
+ }
+
+ public function testPublicationEvent(): void {
+ $this->publicationEvent->expects($this->once())->method('getCalendarData')->with()->willReturn([]);
+ $this->activityBackend->expects($this->once())->method('onCalendarPublication')->with([], true);
+ $this->logger->expects($this->once())->method('debug');
+ $this->calendarPublicationListener->handle($this->publicationEvent);
+ }
+
+ public function testUnPublicationEvent(): void {
+ $this->unpublicationEvent->expects($this->once())->method('getCalendarData')->with()->willReturn([]);
+ $this->activityBackend->expects($this->once())->method('onCalendarPublication')->with([], false);
+ $this->logger->expects($this->once())->method('debug');
+ $this->calendarPublicationListener->handle($this->unpublicationEvent);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php b/apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php
new file mode 100644
index 00000000000..d5697a862db
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Listeners;
+
+use OCA\DAV\CalDAV\Activity\Backend;
+use OCA\DAV\Events\CalendarShareUpdatedEvent;
+use OCA\DAV\Listener\CalendarShareUpdateListener;
+use OCP\EventDispatcher\Event;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class CalendarShareUpdateListenerTest extends TestCase {
+ private Backend&MockObject $activityBackend;
+ private LoggerInterface&MockObject $logger;
+ private CalendarShareUpdateListener $calendarPublicationListener;
+ private CalendarShareUpdatedEvent&MockObject $event;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->activityBackend = $this->createMock(Backend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->event = $this->createMock(CalendarShareUpdatedEvent::class);
+ $this->calendarPublicationListener = new CalendarShareUpdateListener($this->activityBackend, $this->logger);
+ }
+
+ public function testInvalidEvent(): void {
+ $this->activityBackend->expects($this->never())->method('onCalendarUpdateShares');
+ $this->logger->expects($this->never())->method('debug');
+ $this->calendarPublicationListener->handle(new Event());
+ }
+
+ public function testEvent(): void {
+ $this->event->expects($this->once())->method('getCalendarData')->with()->willReturn([]);
+ $this->event->expects($this->once())->method('getOldShares')->with()->willReturn([]);
+ $this->event->expects($this->once())->method('getAdded')->with()->willReturn([]);
+ $this->event->expects($this->once())->method('getRemoved')->with()->willReturn([]);
+ $this->activityBackend->expects($this->once())->method('onCalendarUpdateShares')->with([], [], [], []);
+ $this->logger->expects($this->once())->method('debug');
+ $this->calendarPublicationListener->handle($this->event);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php b/apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php
new file mode 100644
index 00000000000..cbfdfd6b9b7
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Listeners;
+
+use OCA\DAV\BackgroundJob\RefreshWebcalJob;
+use OCA\DAV\CalDAV\Reminder\Backend;
+use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
+use OCA\DAV\Events\SubscriptionCreatedEvent;
+use OCA\DAV\Events\SubscriptionDeletedEvent;
+use OCA\DAV\Listener\SubscriptionListener;
+use OCP\BackgroundJob\IJobList;
+use OCP\EventDispatcher\Event;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class SubscriptionListenerTest extends TestCase {
+ private RefreshWebcalService&MockObject $refreshWebcalService;
+ private Backend&MockObject $reminderBackend;
+ private IJobList&MockObject $jobList;
+ private LoggerInterface&MockObject $logger;
+ private SubscriptionListener $calendarPublicationListener;
+ private SubscriptionCreatedEvent&MockObject $subscriptionCreatedEvent;
+ private SubscriptionDeletedEvent&MockObject $subscriptionDeletedEvent;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->refreshWebcalService = $this->createMock(RefreshWebcalService::class);
+ $this->reminderBackend = $this->createMock(Backend::class);
+ $this->jobList = $this->createMock(IJobList::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->subscriptionCreatedEvent = $this->createMock(SubscriptionCreatedEvent::class);
+ $this->subscriptionDeletedEvent = $this->createMock(SubscriptionDeletedEvent::class);
+ $this->calendarPublicationListener = new SubscriptionListener($this->jobList, $this->refreshWebcalService, $this->reminderBackend, $this->logger);
+ }
+
+ public function testInvalidEvent(): void {
+ $this->refreshWebcalService->expects($this->never())->method('refreshSubscription');
+ $this->jobList->expects($this->never())->method('add');
+ $this->logger->expects($this->never())->method('debug');
+ $this->calendarPublicationListener->handle(new Event());
+ }
+
+ public function testCreateSubscriptionEvent(): void {
+ $this->subscriptionCreatedEvent->expects($this->once())->method('getSubscriptionId')->with()->willReturn(5);
+ $this->subscriptionCreatedEvent->expects($this->once())->method('getSubscriptionData')->with()->willReturn(['principaluri' => 'principaluri', 'uri' => 'uri']);
+ $this->refreshWebcalService->expects($this->once())->method('refreshSubscription')->with('principaluri', 'uri');
+ $this->jobList->expects($this->once())->method('add')->with(RefreshWebcalJob::class, ['principaluri' => 'principaluri', 'uri' => 'uri']);
+ $this->logger->expects($this->exactly(2))->method('debug');
+ $this->calendarPublicationListener->handle($this->subscriptionCreatedEvent);
+ }
+
+ public function testDeleteSubscriptionEvent(): void {
+ $this->subscriptionDeletedEvent->expects($this->once())->method('getSubscriptionId')->with()->willReturn(5);
+ $this->subscriptionDeletedEvent->expects($this->once())->method('getSubscriptionData')->with()->willReturn(['principaluri' => 'principaluri', 'uri' => 'uri']);
+ $this->jobList->expects($this->once())->method('remove')->with(RefreshWebcalJob::class, ['principaluri' => 'principaluri', 'uri' => 'uri']);
+ $this->reminderBackend->expects($this->once())->method('cleanRemindersForCalendar')->with(5);
+ $this->logger->expects($this->exactly(2))->method('debug');
+ $this->calendarPublicationListener->handle($this->subscriptionDeletedEvent);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/OutboxTest.php b/apps/dav/tests/unit/CalDAV/OutboxTest.php
new file mode 100644
index 00000000000..cc0a3f0405f
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/OutboxTest.php
@@ -0,0 +1,105 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\Outbox;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class OutboxTest extends TestCase {
+ private IConfig&MockObject $config;
+ private Outbox $outbox;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->outbox = new Outbox($this->config, 'user-principal-123');
+ }
+
+ public function testGetACLFreeBusyEnabled(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'disableFreeBusy', 'no')
+ ->willReturn('no');
+
+ $this->assertEquals([
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user-principal-123',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user-principal-123/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user-principal-123/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send',
+ 'principal' => 'user-principal-123',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send',
+ 'principal' => 'user-principal-123/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ ], $this->outbox->getACL());
+ }
+
+ public function testGetACLFreeBusyDisabled(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'disableFreeBusy', 'no')
+ ->willReturn('yes');
+
+ $this->assertEquals([
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user-principal-123',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user-principal-123/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user-principal-123/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send-invite',
+ 'principal' => 'user-principal-123',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send-invite',
+ 'principal' => 'user-principal-123/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send-reply',
+ 'principal' => 'user-principal-123',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send-reply',
+ 'principal' => 'user-principal-123/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ ], $this->outbox->getACL());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/PluginTest.php b/apps/dav/tests/unit/CalDAV/PluginTest.php
new file mode 100644
index 00000000000..c5725a1fa81
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/PluginTest.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\Plugin;
+use Test\TestCase;
+
+class PluginTest extends TestCase {
+ private Plugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->plugin = new Plugin();
+ }
+
+ public static function linkProvider(): array {
+ return [
+ [
+ 'principals/users/MyUserName',
+ 'calendars/MyUserName',
+ ],
+ [
+ 'principals/calendar-resources/Resource-ABC',
+ 'system-calendars/calendar-resources/Resource-ABC',
+ ],
+ [
+ 'principals/calendar-rooms/Room-ABC',
+ 'system-calendars/calendar-rooms/Room-ABC',
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('linkProvider')]
+ public function testGetCalendarHomeForPrincipal(string $input, string $expected): void {
+ $this->assertSame($expected, $this->plugin->getCalendarHomeForPrincipal($input));
+ }
+
+ public function testGetCalendarHomeForUnknownPrincipal(): void {
+ $this->assertNull($this->plugin->getCalendarHomeForPrincipal('FOO/BAR/BLUB'));
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php
new file mode 100644
index 00000000000..6acceed6f64
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php
@@ -0,0 +1,141 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\PublicCalendar;
+use OCA\DAV\CalDAV\PublicCalendarRoot;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IUserManager;
+use OCP\Security\ISecureRandom;
+use OCP\Server;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+/**
+ * Class PublicCalendarRootTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\CalDAV
+ */
+class PublicCalendarRootTest extends TestCase {
+ public const UNIT_TEST_USER = '';
+ private CalDavBackend $backend;
+ private PublicCalendarRoot $publicCalendarRoot;
+ private IL10N&MockObject $l10n;
+ private Principal&MockObject $principal;
+ protected IUserManager&MockObject $userManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IConfig&MockObject $config;
+ private ISecureRandom $random;
+ private LoggerInterface&MockObject $logger;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $db = Server::get(IDBConnection::class);
+ $this->principal = $this->createMock('OCA\DAV\Connector\Sabre\Principal');
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->random = Server::get(ISecureRandom::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $dispatcher = $this->createMock(IEventDispatcher::class);
+ $config = $this->createMock(IConfig::class);
+ $sharingBackend = $this->createMock(\OCA\DAV\CalDAV\Sharing\Backend::class);
+
+ $this->principal->expects($this->any())->method('getGroupMembership')
+ ->withAnyParameters()
+ ->willReturn([]);
+
+ $this->principal->expects($this->any())->method('getCircleMembership')
+ ->withAnyParameters()
+ ->willReturn([]);
+
+ $this->backend = new CalDavBackend(
+ $db,
+ $this->principal,
+ $this->userManager,
+ $this->random,
+ $this->logger,
+ $dispatcher,
+ $config,
+ $sharingBackend,
+ false,
+ );
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $this->publicCalendarRoot = new PublicCalendarRoot($this->backend,
+ $this->l10n, $this->config, $this->logger);
+ }
+
+ protected function tearDown(): void {
+ parent::tearDown();
+
+ if (is_null($this->backend)) {
+ return;
+ }
+ $this->principal->expects($this->any())->method('getGroupMembership')
+ ->withAnyParameters()
+ ->willReturn([]);
+
+ $this->principal->expects($this->any())->method('getCircleMembership')
+ ->withAnyParameters()
+ ->willReturn([]);
+
+ $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+ foreach ($books as $book) {
+ $this->backend->deleteCalendar($book['id'], true);
+ }
+ }
+
+ public function testGetName(): void {
+ $name = $this->publicCalendarRoot->getName();
+ $this->assertEquals('public-calendars', $name);
+ }
+
+ public function testGetChild(): void {
+ $calendar = $this->createPublicCalendar();
+
+ $publicCalendars = $this->backend->getPublicCalendars();
+ $this->assertEquals(1, count($publicCalendars));
+ $this->assertEquals(true, $publicCalendars[0]['{http://owncloud.org/ns}public']);
+
+ $publicCalendarURI = $publicCalendars[0]['uri'];
+
+ $calendarResult = $this->publicCalendarRoot->getChild($publicCalendarURI);
+ $this->assertEquals($calendar, $calendarResult);
+ }
+
+ public function testGetChildren(): void {
+ $this->createPublicCalendar();
+ $calendarResults = $this->publicCalendarRoot->getChildren();
+ $this->assertSame([], $calendarResults);
+ }
+
+ protected function createPublicCalendar(): Calendar {
+ $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', []);
+
+ $calendarInfo = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)[0];
+ $calendar = new PublicCalendar($this->backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
+ $publicUri = $calendar->setPublishStatus(true);
+
+ $calendarInfo = $this->backend->getPublicCalendar($publicUri);
+ $calendar = new PublicCalendar($this->backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
+
+ return $calendar;
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/PublicCalendarTest.php b/apps/dav/tests/unit/CalDAV/PublicCalendarTest.php
new file mode 100644
index 00000000000..98153a067fb
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/PublicCalendarTest.php
@@ -0,0 +1,151 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\PublicCalendar;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\VObject\Reader;
+
+class PublicCalendarTest extends CalendarTest {
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')]
+ public function testPrivateClassification(int $expectedChildren, bool $isShared): void {
+ $calObject0 = ['uri' => 'event-0', 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC];
+ $calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL];
+ $calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE];
+
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
+ $backend->expects($this->any())->method('getCalendarObjects')->willReturn([
+ $calObject0, $calObject1, $calObject2
+ ]);
+ $backend->expects($this->any())->method('getMultipleCalendarObjects')
+ ->with(666, ['event-0', 'event-1', 'event-2'])
+ ->willReturn([
+ $calObject0, $calObject1, $calObject2
+ ]);
+ $backend->expects($this->any())->method('getCalendarObject')
+ ->willReturn($calObject2)->with(666, 'event-2');
+ $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1);
+
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user2',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ /** @var IConfig&MockObject $config */
+ $config = $this->createMock(IConfig::class);
+ /** @var LoggerInterface&MockObject $logger */
+ $logger = $this->createMock(LoggerInterface::class);
+ $c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config, $logger);
+ $children = $c->getChildren();
+ $this->assertEquals(2, count($children));
+ $children = $c->getMultipleChildren(['event-0', 'event-1', 'event-2']);
+ $this->assertEquals(2, count($children));
+
+ $this->assertFalse($c->childExists('event-2'));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')]
+ public function testConfidentialClassification(int $expectedChildren, bool $isShared): void {
+ $start = '20160609';
+ $end = '20160610';
+
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//ownCloud calendar v1.2.2
+BEGIN:VEVENT
+CREATED:20160602T133732
+DTSTAMP:20160602T133732
+LAST-MODIFIED:20160602T133732
+UID:wej2z68l9h
+SUMMARY:Test Event
+LOCATION:Somewhere ...
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL;CN=de
+ epdiver:MAILTO:thomas.mueller@tmit.eu
+ORGANIZER;CN=deepdiver:MAILTO:thomas.mueller@tmit.eu
+DESCRIPTION:maybe ....
+DTSTART;TZID=Europe/Berlin;VALUE=DATE:$start
+DTEND;TZID=Europe/Berlin;VALUE=DATE:$end
+RRULE:FREQ=DAILY
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+DTSTART:19810329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+TZNAME:MESZ
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19961027T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+TZNAME:MEZ
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+EOD;
+
+ $calObject0 = ['uri' => 'event-0', 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC];
+ $calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL, 'calendardata' => $calData];
+ $calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE];
+
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
+ $backend->expects($this->any())->method('getCalendarObjects')->willReturn([
+ $calObject0, $calObject1, $calObject2
+ ]);
+ $backend->expects($this->any())->method('getMultipleCalendarObjects')
+ ->with(666, ['event-0', 'event-1', 'event-2'])
+ ->willReturn([
+ $calObject0, $calObject1, $calObject2
+ ]);
+ $backend->expects($this->any())->method('getCalendarObject')
+ ->willReturn($calObject1)->with(666, 'event-1');
+ $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1);
+
+ $calendarInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'cal',
+ ];
+ /** @var IConfig&MockObject $config */
+ $config = $this->createMock(IConfig::class);
+ /** @var LoggerInterface&MockObject $logger */
+ $logger = $this->createMock(LoggerInterface::class);
+ $c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config, $logger);
+
+ $this->assertEquals(count($c->getChildren()), 2);
+
+ // test private event
+ $privateEvent = $c->getChild('event-1');
+ $calData = $privateEvent->get();
+ $event = Reader::read($calData);
+
+ $this->assertEquals($start, $event->VEVENT->DTSTART->getValue());
+ $this->assertEquals($end, $event->VEVENT->DTEND->getValue());
+
+ $this->assertEquals('Busy', $event->VEVENT->SUMMARY->getValue());
+ $this->assertArrayNotHasKey('ATTENDEE', $event->VEVENT);
+ $this->assertArrayNotHasKey('LOCATION', $event->VEVENT);
+ $this->assertArrayNotHasKey('DESCRIPTION', $event->VEVENT);
+ $this->assertArrayNotHasKey('ORGANIZER', $event->VEVENT);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php b/apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php
new file mode 100644
index 00000000000..5344ec5d7cd
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Publishing;
+
+use OCA\DAV\CalDAV\Publishing\Xml\Publisher;
+use Sabre\Xml\Writer;
+use Test\TestCase;
+
+class PublisherTest extends TestCase {
+ public const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
+
+ public function testSerializePublished(): void {
+ $publish = new Publisher('urltopublish', true);
+
+ $xml = $this->write([
+ '{' . self::NS_CALENDARSERVER . '}publish-url' => $publish,
+ ]);
+
+ $this->assertEquals('urltopublish', $publish->getValue());
+
+ $this->assertXmlStringEqualsXmlString(
+ '<?xml version="1.0"?>
+ <x1:publish-url xmlns:d="DAV:" xmlns:x1="' . self::NS_CALENDARSERVER . '">
+ <d:href>urltopublish</d:href>
+ </x1:publish-url>', $xml);
+ }
+
+ public function testSerializeNotPublished(): void {
+ $publish = new Publisher('urltopublish', false);
+
+ $xml = $this->write([
+ '{' . self::NS_CALENDARSERVER . '}pre-publish-url' => $publish,
+ ]);
+
+ $this->assertEquals('urltopublish', $publish->getValue());
+
+ $this->assertXmlStringEqualsXmlString(
+ '<?xml version="1.0"?>
+ <x1:pre-publish-url xmlns:d="DAV:" xmlns:x1="' . self::NS_CALENDARSERVER . '">urltopublish</x1:pre-publish-url>', $xml);
+ }
+
+
+ protected array $elementMap = [];
+ protected array $namespaceMap = ['DAV:' => 'd'];
+ protected string $contextUri = '/';
+
+ private function write($input) {
+ $writer = new Writer();
+ $writer->contextUri = $this->contextUri;
+ $writer->namespaceMap = $this->namespaceMap;
+ $writer->openMemory();
+ $writer->setIndent(true);
+ $writer->write($input);
+ return $writer->outputMemory();
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php b/apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php
new file mode 100644
index 00000000000..ec2ae37a929
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Publishing;
+
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\Publishing\PublishPlugin;
+use OCP\IConfig;
+use OCP\IRequest;
+use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Server;
+use Sabre\DAV\SimpleCollection;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+use Test\TestCase;
+
+class PublishingTest extends TestCase {
+ private PublishPlugin $plugin;
+ private Server $server;
+ private Calendar&MockObject $book;
+ private IConfig&MockObject $config;
+ private IURLGenerator&MockObject $urlGenerator;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->config->expects($this->any())->method('getSystemValue')
+ ->with($this->equalTo('secret'))
+ ->willReturn('mysecret');
+
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+
+ /** @var IRequest $request */
+ $this->plugin = new PublishPlugin($this->config, $this->urlGenerator);
+
+ $root = new SimpleCollection('calendars');
+ $this->server = new Server($root);
+ /** @var SimpleCollection $node */
+ $this->book = $this->getMockBuilder(Calendar::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->book->method('getName')->willReturn('cal1');
+ $root->addChild($this->book);
+ $this->plugin->initialize($this->server);
+ }
+
+ public function testPublishing(): void {
+ $this->book->expects($this->once())->method('setPublishStatus')->with(true);
+
+ // setup request
+ $request = new Request('POST', 'cal1');
+ $request->addHeader('Content-Type', 'application/xml');
+ $request->setBody('<o:publish-calendar xmlns:o="http://calendarserver.org/ns/"/>');
+ $response = new Response();
+ $this->plugin->httpPost($request, $response);
+ }
+
+ public function testUnPublishing(): void {
+ $this->book->expects($this->once())->method('setPublishStatus')->with(false);
+
+ // setup request
+ $request = new Request('POST', 'cal1');
+ $request->addHeader('Content-Type', 'application/xml');
+ $request->setBody('<o:unpublish-calendar xmlns:o="http://calendarserver.org/ns/"/>');
+ $response = new Response();
+ $this->plugin->httpPost($request, $response);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php b/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php
new file mode 100644
index 00000000000..356acf2dd7f
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php
@@ -0,0 +1,377 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Reminder;
+
+use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend;
+use OCP\AppFramework\Utility\ITimeFactory;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class BackendTest extends TestCase {
+ private ReminderBackend $reminderBackend;
+ private ITimeFactory&MockObject $timeFactory;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->delete('calendar_reminders')->executeStatement();
+ $query->delete('calendarobjects')->executeStatement();
+ $query->delete('calendars')->executeStatement();
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->reminderBackend = new ReminderBackend(self::$realDatabase, $this->timeFactory);
+
+ $this->createRemindersTestSet();
+ }
+
+ protected function tearDown(): void {
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->delete('calendar_reminders')->executeStatement();
+ $query->delete('calendarobjects')->executeStatement();
+ $query->delete('calendars')->executeStatement();
+
+ parent::tearDown();
+ }
+
+
+ public function testCleanRemindersForEvent(): void {
+ $query = self::$realDatabase->getQueryBuilder();
+ $rows = $query->select('*')
+ ->from('calendar_reminders')
+ ->execute()
+ ->fetchAll();
+
+ $this->assertCount(4, $rows);
+
+ $this->reminderBackend->cleanRemindersForEvent(1);
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $rows = $query->select('*')
+ ->from('calendar_reminders')
+ ->execute()
+ ->fetchAll();
+
+ $this->assertCount(2, $rows);
+ }
+
+ public function testCleanRemindersForCalendar(): void {
+ $query = self::$realDatabase->getQueryBuilder();
+ $rows = $query->select('*')
+ ->from('calendar_reminders')
+ ->execute()
+ ->fetchAll();
+
+ $this->assertCount(4, $rows);
+
+ $this->reminderBackend->cleanRemindersForCalendar(1);
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $rows = $query->select('*')
+ ->from('calendar_reminders')
+ ->execute()
+ ->fetchAll();
+
+ $this->assertCount(1, $rows);
+ }
+
+ public function testRemoveReminder(): void {
+ $query = self::$realDatabase->getQueryBuilder();
+ $rows = $query->select('*')
+ ->from('calendar_reminders')
+ ->execute()
+ ->fetchAll();
+
+ $this->assertCount(4, $rows);
+
+ $this->reminderBackend->removeReminder((int)$rows[3]['id']);
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $rows = $query->select('*')
+ ->from('calendar_reminders')
+ ->execute()
+ ->fetchAll();
+
+ $this->assertCount(3, $rows);
+ }
+
+ public function testGetRemindersToProcess(): void {
+ $this->timeFactory->expects($this->exactly(1))
+ ->method('getTime')
+ ->with()
+ ->willReturn(123457);
+
+ $rows = $this->reminderBackend->getRemindersToProcess();
+
+ $this->assertCount(2, $rows);
+ unset($rows[0]['id']);
+ unset($rows[1]['id']);
+
+ $expected1 = [
+ 'calendar_id' => 1,
+ 'object_id' => 1,
+ 'uid' => 'asd',
+ 'is_recurring' => false,
+ 'recurrence_id' => 123458,
+ 'is_recurrence_exception' => false,
+ 'event_hash' => 'asd123',
+ 'alarm_hash' => 'asd567',
+ 'type' => 'EMAIL',
+ 'is_relative' => true,
+ 'notification_date' => 123456,
+ 'is_repeat_based' => false,
+ 'calendardata' => 'Calendar data 123',
+ 'displayname' => 'Displayname 123',
+ 'principaluri' => 'principals/users/user001',
+ ];
+ $expected2 = [
+ 'calendar_id' => 1,
+ 'object_id' => 1,
+ 'uid' => 'asd',
+ 'is_recurring' => false,
+ 'recurrence_id' => 123458,
+ 'is_recurrence_exception' => false,
+ 'event_hash' => 'asd123',
+ 'alarm_hash' => 'asd567',
+ 'type' => 'AUDIO',
+ 'is_relative' => true,
+ 'notification_date' => 123456,
+ 'is_repeat_based' => false,
+ 'calendardata' => 'Calendar data 123',
+ 'displayname' => 'Displayname 123',
+ 'principaluri' => 'principals/users/user001',
+ ];
+
+ $this->assertEqualsCanonicalizing([$rows[0],$rows[1]], [$expected1,$expected2]);
+ }
+
+ public function testGetAllScheduledRemindersForEvent(): void {
+ $rows = $this->reminderBackend->getAllScheduledRemindersForEvent(1);
+
+ $this->assertCount(2, $rows);
+ unset($rows[0]['id']);
+ unset($rows[1]['id']);
+
+ $this->assertEquals($rows[0], [
+ 'calendar_id' => 1,
+ 'object_id' => 1,
+ 'uid' => 'asd',
+ 'is_recurring' => false,
+ 'recurrence_id' => 123458,
+ 'is_recurrence_exception' => false,
+ 'event_hash' => 'asd123',
+ 'alarm_hash' => 'asd567',
+ 'type' => 'EMAIL',
+ 'is_relative' => true,
+ 'notification_date' => 123456,
+ 'is_repeat_based' => false,
+ ]);
+ $this->assertEquals($rows[1], [
+ 'calendar_id' => 1,
+ 'object_id' => 1,
+ 'uid' => 'asd',
+ 'is_recurring' => false,
+ 'recurrence_id' => 123458,
+ 'is_recurrence_exception' => false,
+ 'event_hash' => 'asd123',
+ 'alarm_hash' => 'asd567',
+ 'type' => 'AUDIO',
+ 'is_relative' => true,
+ 'notification_date' => 123456,
+ 'is_repeat_based' => false,
+ ]);
+ }
+
+ public function testInsertReminder(): void {
+ $query = self::$realDatabase->getQueryBuilder();
+ $rows = $query->select('*')
+ ->from('calendar_reminders')
+ ->execute()
+ ->fetchAll();
+
+ $this->assertCount(4, $rows);
+
+ $this->reminderBackend->insertReminder(42, 1337, 'uid99', true, 12345678,
+ true, 'hash99', 'hash42', 'AUDIO', false, 12345670, false);
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $rows = $query->select('*')
+ ->from('calendar_reminders')
+ ->execute()
+ ->fetchAll();
+
+ $this->assertCount(5, $rows);
+
+ unset($rows[4]['id']);
+
+ $this->assertEquals($rows[4], [
+ 'calendar_id' => '42',
+ 'object_id' => '1337',
+ 'is_recurring' => '1',
+ 'uid' => 'uid99',
+ 'recurrence_id' => '12345678',
+ 'is_recurrence_exception' => '1',
+ 'event_hash' => 'hash99',
+ 'alarm_hash' => 'hash42',
+ 'type' => 'AUDIO',
+ 'is_relative' => '0',
+ 'notification_date' => '12345670',
+ 'is_repeat_based' => '0',
+ ]);
+ }
+
+ public function testUpdateReminder(): void {
+ $query = self::$realDatabase->getQueryBuilder();
+ $rows = $query->select('*')
+ ->from('calendar_reminders')
+ ->executeQuery()
+ ->fetchAll();
+
+ $this->assertCount(4, $rows);
+
+ $this->assertEquals($rows[3]['notification_date'], 123600);
+
+ $reminderId = (int)$rows[3]['id'];
+ $newNotificationDate = 123700;
+
+ $this->reminderBackend->updateReminder($reminderId, $newNotificationDate);
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $row = $query->select('notification_date')
+ ->from('calendar_reminders')
+ ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId)))
+ ->executeQuery()
+ ->fetch();
+
+ $this->assertEquals((int)$row['notification_date'], 123700);
+ }
+
+
+ private function createRemindersTestSet(): void {
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->insert('calendars')
+ ->values([
+ 'id' => $query->createNamedParameter(1),
+ 'principaluri' => $query->createNamedParameter('principals/users/user001'),
+ 'displayname' => $query->createNamedParameter('Displayname 123'),
+ ])
+ ->executeStatement();
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->insert('calendars')
+ ->values([
+ 'id' => $query->createNamedParameter(99),
+ 'principaluri' => $query->createNamedParameter('principals/users/user002'),
+ 'displayname' => $query->createNamedParameter('Displayname 99'),
+ ])
+ ->executeStatement();
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->insert('calendarobjects')
+ ->values([
+ 'id' => $query->createNamedParameter(1),
+ 'calendardata' => $query->createNamedParameter('Calendar data 123'),
+ 'calendarid' => $query->createNamedParameter(1),
+ 'size' => $query->createNamedParameter(42),
+ ])
+ ->executeStatement();
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->insert('calendarobjects')
+ ->values([
+ 'id' => $query->createNamedParameter(2),
+ 'calendardata' => $query->createNamedParameter('Calendar data 456'),
+ 'calendarid' => $query->createNamedParameter(1),
+ 'size' => $query->createNamedParameter(42),
+ ])
+ ->executeStatement();
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->insert('calendarobjects')
+ ->values([
+ 'id' => $query->createNamedParameter(10),
+ 'calendardata' => $query->createNamedParameter('Calendar data 789'),
+ 'calendarid' => $query->createNamedParameter(99),
+ 'size' => $query->createNamedParameter(42),
+ ])
+ ->executeStatement();
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->insert('calendar_reminders')
+ ->values([
+ 'calendar_id' => $query->createNamedParameter(1),
+ 'object_id' => $query->createNamedParameter(1),
+ 'uid' => $query->createNamedParameter('asd'),
+ 'is_recurring' => $query->createNamedParameter(0),
+ 'recurrence_id' => $query->createNamedParameter(123458),
+ 'is_recurrence_exception' => $query->createNamedParameter(0),
+ 'event_hash' => $query->createNamedParameter('asd123'),
+ 'alarm_hash' => $query->createNamedParameter('asd567'),
+ 'type' => $query->createNamedParameter('EMAIL'),
+ 'is_relative' => $query->createNamedParameter(1),
+ 'notification_date' => $query->createNamedParameter(123456),
+ 'is_repeat_based' => $query->createNamedParameter(0),
+ ])
+ ->executeStatement();
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->insert('calendar_reminders')
+ ->values([
+ 'calendar_id' => $query->createNamedParameter(1),
+ 'object_id' => $query->createNamedParameter(1),
+ 'uid' => $query->createNamedParameter('asd'),
+ 'is_recurring' => $query->createNamedParameter(0),
+ 'recurrence_id' => $query->createNamedParameter(123458),
+ 'is_recurrence_exception' => $query->createNamedParameter(0),
+ 'event_hash' => $query->createNamedParameter('asd123'),
+ 'alarm_hash' => $query->createNamedParameter('asd567'),
+ 'type' => $query->createNamedParameter('AUDIO'),
+ 'is_relative' => $query->createNamedParameter(1),
+ 'notification_date' => $query->createNamedParameter(123456),
+ 'is_repeat_based' => $query->createNamedParameter(0),
+ ])
+ ->executeStatement();
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->insert('calendar_reminders')
+ ->values([
+ 'calendar_id' => $query->createNamedParameter(1),
+ 'object_id' => $query->createNamedParameter(2),
+ 'uid' => $query->createNamedParameter('asd'),
+ 'is_recurring' => $query->createNamedParameter(0),
+ 'recurrence_id' => $query->createNamedParameter(123900),
+ 'is_recurrence_exception' => $query->createNamedParameter(0),
+ 'event_hash' => $query->createNamedParameter('asd123'),
+ 'alarm_hash' => $query->createNamedParameter('asd567'),
+ 'type' => $query->createNamedParameter('EMAIL'),
+ 'is_relative' => $query->createNamedParameter(1),
+ 'notification_date' => $query->createNamedParameter(123499),
+ 'is_repeat_based' => $query->createNamedParameter(0),
+ ])
+ ->executeStatement();
+
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->insert('calendar_reminders')
+ ->values([
+ 'calendar_id' => $query->createNamedParameter(99),
+ 'object_id' => $query->createNamedParameter(10),
+ 'uid' => $query->createNamedParameter('asd'),
+ 'is_recurring' => $query->createNamedParameter(0),
+ 'recurrence_id' => $query->createNamedParameter(123900),
+ 'is_recurrence_exception' => $query->createNamedParameter(0),
+ 'event_hash' => $query->createNamedParameter('asd123'),
+ 'alarm_hash' => $query->createNamedParameter('asd567'),
+ 'type' => $query->createNamedParameter('DISPLAY'),
+ 'is_relative' => $query->createNamedParameter(1),
+ 'notification_date' => $query->createNamedParameter(123600),
+ 'is_repeat_based' => $query->createNamedParameter(0),
+ ])
+ ->executeStatement();
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php
new file mode 100644
index 00000000000..70b374298ea
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider;
+
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\AbstractProvider;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\L10N\IFactory as L10NFactory;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+abstract class AbstractNotificationProviderTestCase extends TestCase {
+ protected LoggerInterface&MockObject $logger;
+ protected L10NFactory&MockObject $l10nFactory;
+ protected IL10N&MockObject $l10n;
+ protected IURLGenerator&MockObject $urlGenerator;
+ protected IConfig&MockObject $config;
+ protected AbstractProvider $provider;
+ protected VCalendar $vcalendar;
+ protected string $calendarDisplayName;
+ protected IUser&MockObject $user;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->l10nFactory = $this->createMock(L10NFactory::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $this->vcalendar = new VCalendar();
+ $this->vcalendar->add('VEVENT', [
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800,
+ 'UID' => 'uid1234',
+ ]);
+ $this->calendarDisplayName = 'Personal';
+
+ $this->user = $this->createMock(IUser::class);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php
new file mode 100644
index 00000000000..d03205eaeb9
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider;
+
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider;
+
+class AudioProviderTest extends PushProviderTest {
+ public function testNotificationType():void {
+ $this->assertEquals(AudioProvider::NOTIFICATION_TYPE, 'AUDIO');
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php
new file mode 100644
index 00000000000..f7fbac2c407
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php
@@ -0,0 +1,509 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider;
+
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\Mail\IEMailTemplate;
+use OCP\Mail\IMailer;
+use OCP\Mail\IMessage;
+use OCP\Util;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+
+class EmailProviderTest extends AbstractNotificationProviderTestCase {
+ public const USER_EMAIL = 'frodo@hobb.it';
+ private IMailer&MockObject $mailer;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->mailer = $this->createMock(IMailer::class);
+
+ $this->provider = new EmailProvider(
+ $this->config,
+ $this->mailer,
+ $this->logger,
+ $this->l10nFactory,
+ $this->urlGenerator
+ );
+ }
+
+ public function testSendWithoutAttendees():void {
+ [$user1, $user2, $user3, , $user5] = $users = $this->getUsers();
+ $principalEmailAddresses = [$user1->getEmailAddress()];
+
+ $enL10N = $this->createMock(IL10N::class);
+ $enL10N->method('t')
+ ->willReturnArgument(0);
+ $enL10N->method('l')
+ ->willReturnArgument(0);
+
+ $deL10N = $this->createMock(IL10N::class);
+ $deL10N->method('t')
+ ->willReturnArgument(0);
+ $deL10N->method('l')
+ ->willReturnArgument(0);
+
+ $this->l10nFactory
+ ->method('getUserLanguage')
+ ->willReturnMap([
+ [$user1, 'en'],
+ [$user2, 'de'],
+ [$user3, 'de'],
+ [$user5, 'de'],
+ ]);
+
+ $this->l10nFactory
+ ->method('findGenericLanguage')
+ ->willReturn('en');
+
+ $this->l10nFactory
+ ->method('languageExists')
+ ->willReturnMap([
+ ['dav', 'en', true],
+ ['dav', 'de', true],
+ ]);
+
+ $this->l10nFactory
+ ->method('get')
+ ->willReturnMap([
+ ['dav', 'en', null, $enL10N],
+ ['dav', 'de', null, $deL10N],
+ ]);
+
+ $template1 = $this->getTemplateMock();
+ $message11 = $this->getMessageMock('uid1@example.com', $template1);
+ $template2 = $this->getTemplateMock();
+ $message21 = $this->getMessageMock('uid2@example.com', $template2);
+ $message22 = $this->getMessageMock('uid3@example.com', $template2);
+
+ $this->mailer->expects($this->exactly(2))
+ ->method('createEMailTemplate')
+ ->with('dav.calendarReminder')
+ ->willReturnOnConsecutiveCalls(
+ $template1,
+ $template2
+ );
+
+ $this->mailer->expects($this->exactly(4))
+ ->method('validateMailAddress')
+ ->willReturnMap([
+ ['uid1@example.com', true],
+ ['uid2@example.com', true],
+ ['uid3@example.com', true],
+ ['invalid', false],
+ ]);
+
+ $this->mailer->expects($this->exactly(3))
+ ->method('createMessage')
+ ->with()
+ ->willReturnOnConsecutiveCalls(
+ $message11,
+ $message21,
+ $message22
+ );
+
+ $calls = [
+ [$message11],
+ [$message21],
+ [$message22],
+ ];
+ $this->mailer->expects($this->exactly(count($calls)))
+ ->method('send')
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return [];
+ });
+
+ $this->setupURLGeneratorMock(2);
+
+ $vcalendar = $this->getNoAttendeeVCalendar();
+ $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users);
+ }
+
+ public function testSendWithAttendeesWhenOwnerIsOrganizer(): void {
+ [$user1, $user2, $user3, , $user5] = $users = $this->getUsers();
+ $principalEmailAddresses = [$user1->getEmailAddress()];
+
+ $enL10N = $this->createMock(IL10N::class);
+ $enL10N->method('t')
+ ->willReturnArgument(0);
+ $enL10N->method('l')
+ ->willReturnArgument(0);
+
+ $deL10N = $this->createMock(IL10N::class);
+ $deL10N->method('t')
+ ->willReturnArgument(0);
+ $deL10N->method('l')
+ ->willReturnArgument(0);
+
+ $this->l10nFactory
+ ->method('getUserLanguage')
+ ->willReturnMap([
+ [$user1, 'en'],
+ [$user2, 'de'],
+ [$user3, 'de'],
+ [$user5, 'de'],
+ ]);
+
+ $this->l10nFactory
+ ->method('findGenericLanguage')
+ ->willReturn('en');
+
+ $this->l10nFactory
+ ->method('languageExists')
+ ->willReturnMap([
+ ['dav', 'en', true],
+ ['dav', 'de', true],
+ ]);
+
+ $this->l10nFactory
+ ->method('get')
+ ->willReturnMap([
+ ['dav', 'en', null, $enL10N],
+ ['dav', 'de', null, $deL10N],
+ ]);
+
+ $template1 = $this->getTemplateMock();
+ $message11 = $this->getMessageMock('foo1@example.org', $template1);
+ $message12 = $this->getMessageMock('uid2@example.com', $template1);
+ $message13 = $this->getMessageMock('uid3@example.com', $template1);
+ $template2 = $this->getTemplateMock();
+ $message21 = $this->getMessageMock('foo3@example.org', $template2);
+ $message22 = $this->getMessageMock('foo4@example.org', $template2);
+ $message23 = $this->getMessageMock('uid1@example.com', $template2);
+
+ $this->mailer->expects(self::exactly(2))
+ ->method('createEMailTemplate')
+ ->with('dav.calendarReminder')
+ ->willReturnOnConsecutiveCalls(
+ $template1,
+ $template2,
+ );
+ $this->mailer->expects($this->atLeastOnce())
+ ->method('validateMailAddress')
+ ->willReturnMap([
+ ['foo1@example.org', true],
+ ['foo3@example.org', true],
+ ['foo4@example.org', true],
+ ['uid1@example.com', true],
+ ['uid2@example.com', true],
+ ['uid3@example.com', true],
+ ['invalid', false],
+ ]);
+ $this->mailer->expects($this->exactly(6))
+ ->method('createMessage')
+ ->with()
+ ->willReturnOnConsecutiveCalls(
+ $message11,
+ $message12,
+ $message13,
+ $message21,
+ $message22,
+ $message23,
+ );
+
+ $calls = [
+ [$message11],
+ [$message12],
+ [$message13],
+ [$message21],
+ [$message22],
+ [$message23],
+ ];
+ $this->mailer->expects($this->exactly(count($calls)))
+ ->method('send')
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return [];
+ });
+ $this->setupURLGeneratorMock(2);
+
+ $vcalendar = $this->getAttendeeVCalendar();
+ $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users);
+ }
+
+ public function testSendWithAttendeesWhenOwnerIsAttendee(): void {
+ [$user1, $user2, $user3] = $this->getUsers();
+ $users = [$user2, $user3];
+ $principalEmailAddresses = [$user2->getEmailAddress()];
+
+ $deL10N = $this->createMock(IL10N::class);
+ $deL10N->method('t')
+ ->willReturnArgument(0);
+ $deL10N->method('l')
+ ->willReturnArgument(0);
+
+ $this->l10nFactory
+ ->method('getUserLanguage')
+ ->willReturnMap([
+ [$user2, 'de'],
+ [$user3, 'de'],
+ ]);
+
+ $this->l10nFactory
+ ->method('findGenericLanguage')
+ ->willReturn('en');
+
+ $this->l10nFactory
+ ->method('languageExists')
+ ->willReturnMap([
+ ['dav', 'de', true],
+ ]);
+
+ $this->l10nFactory
+ ->method('get')
+ ->willReturnMap([
+ ['dav', 'de', null, $deL10N],
+ ]);
+
+ $template1 = $this->getTemplateMock();
+ $message12 = $this->getMessageMock('uid2@example.com', $template1);
+ $message13 = $this->getMessageMock('uid3@example.com', $template1);
+
+ $this->mailer->expects(self::once())
+ ->method('createEMailTemplate')
+ ->with('dav.calendarReminder')
+ ->willReturnOnConsecutiveCalls(
+ $template1,
+ );
+ $this->mailer->expects($this->atLeastOnce())
+ ->method('validateMailAddress')
+ ->willReturnMap([
+ ['foo1@example.org', true],
+ ['foo3@example.org', true],
+ ['foo4@example.org', true],
+ ['uid1@example.com', true],
+ ['uid2@example.com', true],
+ ['uid3@example.com', true],
+ ['invalid', false],
+ ]);
+ $this->mailer->expects($this->exactly(2))
+ ->method('createMessage')
+ ->with()
+ ->willReturnOnConsecutiveCalls(
+ $message12,
+ $message13,
+ );
+
+ $calls = [
+ [$message12],
+ [$message13],
+ ];
+ $this->mailer->expects($this->exactly(count($calls)))
+ ->method('send')
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return [];
+ });
+ $this->setupURLGeneratorMock(1);
+
+ $vcalendar = $this->getAttendeeVCalendar();
+ $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users);
+ }
+
+ /**
+ * @return IEMailTemplate
+ */
+ private function getTemplateMock():IEMailTemplate {
+ $template = $this->createMock(IEMailTemplate::class);
+
+ $template->expects($this->once())
+ ->method('addHeader')
+ ->with()
+ ->willReturn($template);
+
+ $template->expects($this->once())
+ ->method('setSubject')
+ ->with()
+ ->willReturn($template);
+
+ $template->expects($this->once())
+ ->method('addHeading')
+ ->with()
+ ->willReturn($template);
+
+ $template->expects($this->exactly(4))
+ ->method('addBodyListItem')
+ ->with()
+ ->willReturn($template);
+
+ $template->expects($this->once())
+ ->method('addFooter')
+ ->with()
+ ->willReturn($template);
+
+ return $template;
+ }
+
+ /**
+ * @param string $toMail
+ * @param IEMailTemplate $templateMock
+ * @param array|null $replyTo
+ * @return IMessage
+ */
+ private function getMessageMock(string $toMail, IEMailTemplate $templateMock, ?array $replyTo = null):IMessage {
+ $message = $this->createMock(IMessage::class);
+ $i = 0;
+
+ $message->expects($this->once())
+ ->method('setFrom')
+ ->with([Util::getDefaultEmailAddress('reminders-noreply')])
+ ->willReturn($message);
+
+ if ($replyTo) {
+ $message->expects($this->once())
+ ->method('setReplyTo')
+ ->with($replyTo)
+ ->willReturn($message);
+ } else {
+ $message->expects($this->never())
+ ->method('setReplyTo');
+ }
+
+ $message->expects($this->once())
+ ->method('setTo')
+ ->with([$toMail])
+ ->willReturn($message);
+
+ $message->expects($this->once())
+ ->method('useTemplate')
+ ->with($templateMock)
+ ->willReturn($message);
+
+ return $message;
+ }
+
+ private function getNoAttendeeVCalendar():VCalendar {
+ $vcalendar = new VCalendar();
+ $vcalendar->add('VEVENT', [
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800,
+ 'UID' => 'uid1234',
+ 'LOCATION' => 'Location 123',
+ 'DESCRIPTION' => 'DESCRIPTION 456',
+ ]);
+
+ return $vcalendar;
+ }
+
+ private function getAttendeeVCalendar():VCalendar {
+ $vcalendar = new VCalendar();
+ $vcalendar->add('VEVENT', [
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800,
+ 'UID' => 'uid1234',
+ 'LOCATION' => 'Location 123',
+ 'DESCRIPTION' => 'DESCRIPTION 456',
+ ]);
+
+ $vcalendar->VEVENT->add(
+ 'ORGANIZER',
+ 'mailto:uid1@example.com',
+ [
+ 'LANG' => 'en'
+ ]
+ );
+
+ $vcalendar->VEVENT->add(
+ 'ATTENDEE',
+ 'mailto:foo1@example.org',
+ [
+ 'LANG' => 'de',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ ]
+ );
+
+ $vcalendar->VEVENT->add(
+ 'ATTENDEE',
+ 'mailto:foo2@example.org',
+ [
+ 'LANG' => 'de',
+ 'PARTSTAT' => 'DECLINED',
+ ]
+ );
+
+ $vcalendar->VEVENT->add(
+ 'ATTENDEE',
+ 'mailto:foo3@example.org',
+ [
+ 'LANG' => 'en',
+ 'PARTSTAT' => 'CONFIRMED',
+ ]
+ );
+
+ $vcalendar->VEVENT->add(
+ 'ATTENDEE',
+ 'mailto:foo4@example.org'
+ );
+
+ $vcalendar->VEVENT->add(
+ 'ATTENDEE',
+ 'tomail:foo5@example.org'
+ );
+
+ return $vcalendar;
+ }
+
+ private function setupURLGeneratorMock(int $times = 1): void {
+ $this->urlGenerator
+ ->expects($this->exactly($times * 4))
+ ->method('imagePath')
+ ->willReturnMap([
+ ['core', 'actions/info.png', 'imagePath1'],
+ ['core', 'places/calendar.png', 'imagePath2'],
+ ['core', 'actions/address.png', 'imagePath3'],
+ ['core', 'actions/more.png', 'imagePath4'],
+ ]);
+ $this->urlGenerator
+ ->expects($this->exactly($times * 4))
+ ->method('getAbsoluteURL')
+ ->willReturnMap([
+ ['imagePath1', 'AbsURL1'],
+ ['imagePath2', 'AbsURL2'],
+ ['imagePath3', 'AbsURL3'],
+ ['imagePath4', 'AbsURL4'],
+ ]);
+ }
+
+ private function getUsers(): array {
+ $user1 = $this->createMock(IUser::class);
+ $user1->method('getUID')
+ ->willReturn('uid1');
+ $user1->method('getEMailAddress')
+ ->willReturn('uid1@example.com');
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')
+ ->willReturn('uid2');
+ $user2->method('getEMailAddress')
+ ->willReturn('uid2@example.com');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')
+ ->willReturn('uid3');
+ $user3->method('getEMailAddress')
+ ->willReturn('uid3@example.com');
+ $user4 = $this->createMock(IUser::class);
+ $user4->method('getUID')
+ ->willReturn('uid4');
+ $user4->method('getEMailAddress')
+ ->willReturn(null);
+ $user5 = $this->createMock(IUser::class);
+ $user5->method('getUID')
+ ->willReturn('uid5');
+ $user5->method('getEMailAddress')
+ ->willReturn('invalid');
+
+ return [$user1, $user2, $user3, $user4, $user5];
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php
new file mode 100644
index 00000000000..5034af49cae
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php
@@ -0,0 +1,173 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider;
+
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IUser;
+use OCP\Notification\IManager;
+use OCP\Notification\INotification;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class PushProviderTest extends AbstractNotificationProviderTestCase {
+ private IManager&MockObject $manager;
+ private ITimeFactory&MockObject $timeFactory;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->manager = $this->createMock(IManager::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+
+ $this->provider = new PushProvider(
+ $this->config,
+ $this->manager,
+ $this->logger,
+ $this->l10nFactory,
+ $this->urlGenerator,
+ $this->timeFactory
+ );
+ }
+
+ public function testNotificationType():void {
+ $this->assertEquals(PushProvider::NOTIFICATION_TYPE, 'DISPLAY');
+ }
+
+ public function testNotSend(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'sendEventRemindersPush', 'yes')
+ ->willReturn('no');
+
+ $this->manager->expects($this->never())
+ ->method('createNotification');
+ $this->manager->expects($this->never())
+ ->method('notify');
+
+ $user1 = $this->createMock(IUser::class);
+ $user1->method('getUID')
+ ->willReturn('uid1');
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')
+ ->willReturn('uid2');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')
+ ->willReturn('uid3');
+
+ $users = [$user1, $user2, $user3];
+
+ $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, [], $users);
+ }
+
+ public function testSend(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'sendEventRemindersPush', 'yes')
+ ->willReturn('yes');
+
+ $user1 = $this->createMock(IUser::class);
+ $user1->method('getUID')
+ ->willReturn('uid1');
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')
+ ->willReturn('uid2');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')
+ ->willReturn('uid3');
+
+ $users = [$user1, $user2, $user3];
+
+ $dateTime = new \DateTime('@946684800');
+ $this->timeFactory->method('getDateTime')
+ ->with()
+ ->willReturn($dateTime);
+
+ $notification1 = $this->createNotificationMock('uid1', $dateTime);
+ $notification2 = $this->createNotificationMock('uid2', $dateTime);
+ $notification3 = $this->createNotificationMock('uid3', $dateTime);
+
+ $this->manager->expects($this->exactly(3))
+ ->method('createNotification')
+ ->willReturnOnConsecutiveCalls(
+ $notification1,
+ $notification2,
+ $notification3
+ );
+
+ $calls = [
+ $notification1,
+ $notification2,
+ $notification3,
+ ];
+ $this->manager->expects($this->exactly(3))
+ ->method('notify')
+ ->willReturnCallback(function ($notification) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, $notification);
+ });
+
+ $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, [], $users);
+ }
+
+ /**
+ * @param string $uid
+ * @param \DateTime $dt
+ */
+ private function createNotificationMock(string $uid, \DateTime $dt):INotification {
+ $notification = $this->createMock(INotification::class);
+ $notification
+ ->expects($this->once())
+ ->method('setApp')
+ ->with('dav')
+ ->willReturn($notification);
+
+ $notification->expects($this->once())
+ ->method('setUser')
+ ->with($uid)
+ ->willReturn($notification);
+
+ $notification->expects($this->once())
+ ->method('setDateTime')
+ ->with($dt)
+ ->willReturn($notification);
+
+ $notification->expects($this->once())
+ ->method('setObject')
+ ->with('dav', hash('sha256', 'uid1234', false))
+ ->willReturn($notification);
+
+ $notification->expects($this->once())
+ ->method('setSubject')
+ ->with('calendar_reminder', [
+ 'title' => 'Fellowship meeting',
+ 'start_atom' => '2017-01-01T00:00:00+00:00',
+ ])
+ ->willReturn($notification);
+
+ $notification
+ ->expects($this->once())
+ ->method('setMessage')
+ ->with('calendar_reminder', [
+ 'title' => 'Fellowship meeting',
+ 'start_atom' => '2017-01-01T00:00:00+00:00',
+ 'description' => null,
+ 'location' => null,
+ 'all_day' => false,
+ 'start_is_floating' => false,
+ 'start_timezone' => 'UTC',
+ 'end_atom' => '2017-01-01T00:00:00+00:00',
+ 'end_is_floating' => false,
+ 'end_timezone' => 'UTC',
+ 'calendar_displayname' => 'Personal',
+ ])
+ ->willReturn($notification);
+
+ return $notification;
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php
new file mode 100644
index 00000000000..6b813ed0228
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php
@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Reminder;
+
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider;
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException;
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider;
+use OCA\DAV\CalDAV\Reminder\NotificationProviderManager;
+use OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException;
+use OCA\DAV\Capabilities;
+use OCP\AppFramework\QueryException;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class NotificationProviderManagerTest extends TestCase {
+ private NotificationProviderManager $providerManager;
+
+ /**
+ * @throws QueryException
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->providerManager = new NotificationProviderManager();
+ $this->providerManager->registerProvider(EmailProvider::class);
+ }
+
+ /**
+ * @throws ProviderNotAvailableException
+ * @throws NotificationTypeDoesNotExistException
+ */
+ public function testGetProviderForUnknownType(): void {
+ $this->expectException(NotificationTypeDoesNotExistException::class);
+ $this->expectExceptionMessage('Type NOT EXISTENT is not an accepted type of notification');
+
+ $this->providerManager->getProvider('NOT EXISTENT');
+ }
+
+ /**
+ * @throws NotificationTypeDoesNotExistException
+ * @throws ProviderNotAvailableException
+ */
+ public function testGetProviderForUnRegisteredType(): void {
+ $this->expectException(ProviderNotAvailableException::class);
+ $this->expectExceptionMessage('No notification provider for type AUDIO available');
+
+ $this->providerManager->getProvider('AUDIO');
+ }
+
+ public function testGetProvider(): void {
+ $provider = $this->providerManager->getProvider('EMAIL');
+ $this->assertInstanceOf(EmailProvider::class, $provider);
+ }
+
+ public function testRegisterProvider(): void {
+ $this->providerManager->registerProvider(PushProvider::class);
+ $provider = $this->providerManager->getProvider('DISPLAY');
+ $this->assertInstanceOf(PushProvider::class, $provider);
+ }
+
+ /**
+ * @throws QueryException
+ */
+ public function testRegisterBadProvider(): void {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid notification provider registered');
+
+ $this->providerManager->registerProvider(Capabilities::class);
+ }
+
+ public function testHasProvider(): void {
+ $this->assertTrue($this->providerManager->hasProvider('EMAIL'));
+ $this->assertFalse($this->providerManager->hasProvider('EMAIL123'));
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php
new file mode 100644
index 00000000000..c091f590711
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php
@@ -0,0 +1,263 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Reminder;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CalDAV\Reminder\Notifier;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\L10N\IFactory;
+use OCP\Notification\AlreadyProcessedException;
+use OCP\Notification\INotification;
+use OCP\Notification\UnknownNotificationException;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class NotifierTest extends TestCase {
+ protected IFactory&MockObject $factory;
+ protected IURLGenerator&MockObject $urlGenerator;
+ protected IL10N&MockObject $l10n;
+ protected ITimeFactory&MockObject $timeFactory;
+ protected Notifier $notifier;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->l10n->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($string, $args) {
+ if (!is_array($args)) {
+ $args = [$args];
+ }
+ return vsprintf($string, $args);
+ });
+ $this->l10n->expects($this->any())
+ ->method('l')
+ ->willReturnCallback(function ($string, $args) {
+ /** \DateTime $args */
+ return $args->format(\DateTime::ATOM);
+ });
+ $this->l10n->expects($this->any())
+ ->method('n')
+ ->willReturnCallback(function ($textSingular, $textPlural, $count, $args) {
+ $text = $count === 1 ? $textSingular : $textPlural;
+ $text = str_replace('%n', (string)$count, $text);
+ return vsprintf($text, $args);
+ });
+ $this->factory = $this->createMock(IFactory::class);
+ $this->factory->expects($this->any())
+ ->method('get')
+ ->willReturn($this->l10n);
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->timeFactory
+ ->method('getDateTime')
+ ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2005-08-15T14:00:00+02:00'));
+
+ $this->notifier = new Notifier(
+ $this->factory,
+ $this->urlGenerator,
+ $this->timeFactory
+ );
+ }
+
+ public function testGetId():void {
+ $this->assertEquals($this->notifier->getID(), 'dav');
+ }
+
+ public function testGetName():void {
+ $this->assertEquals($this->notifier->getName(), 'Calendar');
+ }
+
+
+ public function testPrepareWrongApp(): void {
+ $this->expectException(UnknownNotificationException::class);
+ $this->expectExceptionMessage('Notification not from this app');
+
+ /** @var INotification&MockObject $notification */
+ $notification = $this->createMock(INotification::class);
+
+ $notification->expects($this->once())
+ ->method('getApp')
+ ->willReturn('notifications');
+ $notification->expects($this->never())
+ ->method('getSubject');
+
+ $this->notifier->prepare($notification, 'en');
+ }
+
+
+ public function testPrepareWrongSubject(): void {
+ $this->expectException(UnknownNotificationException::class);
+ $this->expectExceptionMessage('Unknown subject');
+
+ /** @var INotification&MockObject $notification */
+ $notification = $this->createMock(INotification::class);
+
+ $notification->expects($this->once())
+ ->method('getApp')
+ ->willReturn(Application::APP_ID);
+ $notification->expects($this->once())
+ ->method('getSubject')
+ ->willReturn('wrong subject');
+
+ $this->notifier->prepare($notification, 'en');
+ }
+
+ private static function hasPhpDatetimeDiffBug(): bool {
+ $d1 = \DateTime::createFromFormat(\DateTimeInterface::ATOM, '2023-11-22T11:52:00+01:00');
+ $d2 = new \DateTime('2023-11-22T10:52:03', new \DateTimeZone('UTC'));
+
+ // The difference is 3 seconds, not -1year+11months+…
+ return $d1->diff($d2)->y < 0;
+ }
+
+ public static function dataPrepare(): array {
+ return [
+ [
+ 'calendar_reminder',
+ [
+ 'title' => 'Title of this event',
+ 'start_atom' => '2005-08-15T15:52:01+02:00'
+ ],
+ self::hasPhpDatetimeDiffBug() ? 'Title of this event' : 'Title of this event (in 1 hour, 52 minutes)',
+ [
+ 'title' => 'Title of this event',
+ 'description' => null,
+ 'location' => 'NC Headquarters',
+ 'all_day' => false,
+ 'start_atom' => '2005-08-15T15:52:01+02:00',
+ 'start_is_floating' => false,
+ 'start_timezone' => 'Europe/Berlin',
+ 'end_atom' => '2005-08-15T17:52:01+02:00',
+ 'end_is_floating' => false,
+ 'end_timezone' => 'Europe/Berlin',
+ 'calendar_displayname' => 'Personal',
+ ],
+ "Calendar: Personal\r\nDate: 2005-08-15T15:52:01+02:00, 2005-08-15T15:52:01+02:00 - 2005-08-15T17:52:01+02:00 (Europe/Berlin)\r\nWhere: NC Headquarters"
+ ],
+ [
+ 'calendar_reminder',
+ [
+ 'title' => 'Title of this event',
+ 'start_atom' => '2005-08-15T13:00:00+02:00',
+ ],
+ self::hasPhpDatetimeDiffBug() ? 'Title of this event' : 'Title of this event (1 hour ago)',
+ [
+ 'title' => 'Title of this event',
+ 'description' => null,
+ 'location' => 'NC Headquarters',
+ 'all_day' => false,
+ 'start_atom' => '2005-08-15T13:00:00+02:00',
+ 'start_is_floating' => false,
+ 'start_timezone' => 'Europe/Berlin',
+ 'end_atom' => '2005-08-15T15:00:00+02:00',
+ 'end_is_floating' => false,
+ 'end_timezone' => 'Europe/Berlin',
+ 'calendar_displayname' => 'Personal',
+ ],
+ "Calendar: Personal\r\nDate: 2005-08-15T13:00:00+02:00, 2005-08-15T13:00:00+02:00 - 2005-08-15T15:00:00+02:00 (Europe/Berlin)\r\nWhere: NC Headquarters"
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataPrepare')]
+ public function testPrepare(string $subjectType, array $subjectParams, string $subject, array $messageParams, string $message): void {
+ /** @var INotification&MockObject $notification */
+ $notification = $this->createMock(INotification::class);
+
+ $notification->expects($this->once())
+ ->method('getApp')
+ ->willReturn(Application::APP_ID);
+ $notification->expects($this->once())
+ ->method('getSubject')
+ ->willReturn($subjectType);
+ $notification->expects($this->once())
+ ->method('getSubjectParameters')
+ ->willReturn($subjectParams);
+ $notification->expects($this->once())
+ ->method('getMessageParameters')
+ ->willReturn($messageParams);
+
+ $notification->expects($this->once())
+ ->method('setParsedSubject')
+ ->with($subject)
+ ->willReturnSelf();
+
+ $notification->expects($this->once())
+ ->method('setParsedMessage')
+ ->with($message)
+ ->willReturnSelf();
+
+ $this->urlGenerator->expects($this->once())
+ ->method('imagePath')
+ ->with('core', 'places/calendar.svg')
+ ->willReturn('icon-url');
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('icon-url')
+ ->willReturn('absolute-icon-url');
+ $notification->expects($this->once())
+ ->method('setIcon')
+ ->with('absolute-icon-url')
+ ->willReturnSelf();
+
+ $return = $this->notifier->prepare($notification, 'en');
+
+ $this->assertEquals($notification, $return);
+ }
+
+ public function testPassedEvent(): void {
+ /** @var INotification&MockObject $notification */
+ $notification = $this->createMock(INotification::class);
+
+ $notification->expects($this->once())
+ ->method('getApp')
+ ->willReturn(Application::APP_ID);
+ $notification->expects($this->once())
+ ->method('getSubject')
+ ->willReturn('calendar_reminder');
+ $notification->expects($this->once())
+ ->method('getSubjectParameters')
+ ->willReturn([
+ 'title' => 'Title of this event',
+ 'start_atom' => '2005-08-15T08:00:00+02:00'
+ ]);
+
+ $notification->expects($this->once())
+ ->method('getMessageParameters')
+ ->willReturn([
+ 'title' => 'Title of this event',
+ 'description' => null,
+ 'location' => 'NC Headquarters',
+ 'all_day' => false,
+ 'start_atom' => '2005-08-15T08:00:00+02:00',
+ 'start_is_floating' => false,
+ 'start_timezone' => 'Europe/Berlin',
+ 'end_atom' => '2005-08-15T13:00:00+02:00',
+ 'end_is_floating' => false,
+ 'end_timezone' => 'Europe/Berlin',
+ 'calendar_displayname' => 'Personal',
+ ]);
+
+ $notification->expects($this->once())
+ ->method('setParsedSubject')
+ ->with(self::hasPhpDatetimeDiffBug() ? 'Title of this event' : 'Title of this event (6 hours ago)')
+ ->willReturnSelf();
+
+ $this->expectException(AlreadyProcessedException::class);
+
+ $return = $this->notifier->prepare($notification, 'en');
+
+ $this->assertEquals($notification, $return);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php b/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php
new file mode 100644
index 00000000000..c18901c5f58
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php
@@ -0,0 +1,777 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Reminder;
+
+use DateTime;
+use DateTimeZone;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Reminder\Backend;
+use OCA\DAV\CalDAV\Reminder\INotificationProvider;
+use OCA\DAV\CalDAV\Reminder\NotificationProviderManager;
+use OCA\DAV\CalDAV\Reminder\ReminderService;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class ReminderServiceTest extends TestCase {
+ private Backend&MockObject $backend;
+ private NotificationProviderManager&MockObject $notificationProviderManager;
+ private IUserManager&MockObject $userManager;
+ private IGroupManager&MockObject $groupManager;
+ private CalDavBackend&MockObject $caldavBackend;
+ private ITimeFactory&MockObject $timeFactory;
+ private IConfig&MockObject $config;
+ private LoggerInterface&MockObject $logger;
+ private Principal&MockObject $principalConnector;
+ private ReminderService $reminderService;
+
+ public const CALENDAR_DATA = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud calendar v1.6.4
+BEGIN:VEVENT
+CREATED:20160602T133732
+DTSTAMP:20160602T133732
+LAST-MODIFIED:20160602T133732
+UID:wej2z68l9h
+SUMMARY:Test Event
+LOCATION:Somewhere ...
+DESCRIPTION:maybe ....
+DTSTART;TZID=Europe/Berlin;VALUE=DATE:20160609
+DTEND;TZID=Europe/Berlin;VALUE=DATE:20160610
+BEGIN:VALARM
+ACTION:EMAIL
+TRIGGER:-PT15M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;VALUE=DATE-TIME:20160608T000000Z
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ public const CALENDAR_DATA_REPEAT = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud calendar v1.6.4
+BEGIN:VEVENT
+CREATED:20160602T133732
+DTSTAMP:20160602T133732
+LAST-MODIFIED:20160602T133732
+UID:wej2z68l9h
+SUMMARY:Test Event
+LOCATION:Somewhere ...
+DESCRIPTION:maybe ....
+DTSTART;TZID=Europe/Berlin;VALUE=DATE:20160609
+DTEND;TZID=Europe/Berlin;VALUE=DATE:20160610
+BEGIN:VALARM
+ACTION:EMAIL
+TRIGGER:-PT15M
+REPEAT:4
+DURATION:PT2M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ public const CALENDAR_DATA_RECURRING = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud calendar v1.6.4
+BEGIN:VEVENT
+CREATED:20160602T133732
+DTSTAMP:20160602T133732
+LAST-MODIFIED:20160602T133732
+UID:wej2z68l9h
+SUMMARY:Test Event
+LOCATION:Somewhere ...
+DESCRIPTION:maybe ....
+DTSTART;TZID=Europe/Berlin;VALUE=DATE:20160609
+DTEND;TZID=Europe/Berlin;VALUE=DATE:20160610
+RRULE:FREQ=WEEKLY
+BEGIN:VALARM
+ACTION:EMAIL
+TRIGGER:-PT15M
+END:VALARM
+BEGIN:VALARM
+ACTION:EMAIL
+TRIGGER:-P8D
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ public const CALENDAR_DATA_RECURRING_REPEAT = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud calendar v1.6.4
+BEGIN:VEVENT
+CREATED:20160602T133732
+DTSTAMP:20160602T133732
+LAST-MODIFIED:20160602T133732
+UID:wej2z68l9h
+SUMMARY:Test Event
+LOCATION:Somewhere ...
+DESCRIPTION:maybe ....
+DTSTART;TZID=Europe/Berlin;VALUE=DATE:20160609
+DTEND;TZID=Europe/Berlin;VALUE=DATE:20160610
+RRULE:FREQ=WEEKLY
+BEGIN:VALARM
+ACTION:EMAIL
+TRIGGER:-PT15M
+REPEAT:4
+DURATION:PT2M
+END:VALARM
+BEGIN:VALARM
+ACTION:EMAIL
+TRIGGER:-P8D
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ public const CALENDAR_DATA_NO_ALARM = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud calendar v1.6.4
+BEGIN:VEVENT
+CREATED:20160602T133732
+DTSTAMP:20160602T133732
+LAST-MODIFIED:20160602T133732
+UID:wej2z68l9h
+SUMMARY:Test Event
+LOCATION:Somewhere ...
+DESCRIPTION:maybe ....
+DTSTART;TZID=Europe/Berlin;VALUE=DATE:20160609
+DTEND;TZID=Europe/Berlin;VALUE=DATE:20160610
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ private const CALENDAR_DATA_ONE_TIME = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//IDN nextcloud.com//Calendar app 4.3.0-alpha.0//EN
+CALSCALE:GREGORIAN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20230203T154600Z
+DTSTAMP:20230203T154602Z
+LAST-MODIFIED:20230203T154602Z
+SEQUENCE:2
+UID:f6a565b6-f9a8-4d1e-9d01-c8dcbe716b7e
+DTSTART;TZID=Europe/Vienna:20230204T090000
+DTEND;TZID=Europe/Vienna:20230204T120000
+STATUS:CONFIRMED
+SUMMARY:TEST
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;RELATED=START:-PT1H
+END:VALARM
+END:VEVENT
+BEGIN:VTIMEZONE
+TZID:Europe/Vienna
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+EOD;
+
+ private const CALENDAR_DATA_ALL_DAY = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//IDN nextcloud.com//Calendar app 4.3.0-alpha.0//EN
+CALSCALE:GREGORIAN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20230203T113430Z
+DTSTAMP:20230203T113432Z
+LAST-MODIFIED:20230203T113432Z
+SEQUENCE:2
+UID:a163a056-ba26-44a2-8080-955f19611a8f
+DTSTART;VALUE=DATE:20230204
+DTEND;VALUE=DATE:20230205
+STATUS:CONFIRMED
+SUMMARY:TEST
+BEGIN:VALARM
+ACTION:EMAIL
+TRIGGER;RELATED=START:-PT1H
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ private const PAGO_PAGO_VTIMEZONE_ICS = <<<ICS
+BEGIN:VCALENDAR
+BEGIN:VTIMEZONE
+TZID:Pacific/Pago_Pago
+BEGIN:STANDARD
+TZOFFSETFROM:-1100
+TZOFFSETTO:-1100
+TZNAME:SST
+DTSTART:19700101T000000
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+ICS;
+
+ private ?string $oldTimezone;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->backend = $this->createMock(Backend::class);
+ $this->notificationProviderManager = $this->createMock(NotificationProviderManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->caldavBackend = $this->createMock(CalDavBackend::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->principalConnector = $this->createMock(Principal::class);
+
+ $this->caldavBackend->method('getShares')->willReturn([]);
+
+ $this->reminderService = new ReminderService(
+ $this->backend,
+ $this->notificationProviderManager,
+ $this->userManager,
+ $this->groupManager,
+ $this->caldavBackend,
+ $this->timeFactory,
+ $this->config,
+ $this->logger,
+ $this->principalConnector,
+ );
+ }
+
+ public function testOnCalendarObjectDelete():void {
+ $this->backend->expects($this->once())
+ ->method('cleanRemindersForEvent')
+ ->with(44);
+
+ $objectData = [
+ 'id' => '44',
+ 'component' => 'vevent',
+ ];
+
+ $this->reminderService->onCalendarObjectDelete($objectData);
+ }
+
+ public function testOnCalendarObjectCreateSingleEntry():void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+
+ $calls = [
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'de919af7429d3b5c11e8b9d289b411a6', 'EMAIL', true, 1465429500, false],
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', '35b3eae8e792aa2209f0b4e1a302f105', 'DISPLAY', false, 1465344000, false]
+ ];
+ $this->backend->expects($this->exactly(count($calls)))
+ ->method('insertReminder')
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return 1;
+ });
+
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00'));
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ /**
+ * RFC5545 says DTSTART is REQUIRED, but we have seen event without the prop
+ */
+ public function testOnCalendarObjectCreateNoDtstart(): void {
+ $calendarData = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud calendar v1.6.4
+BEGIN:VEVENT
+CREATED:20160602T133732
+DTSTAMP:20160602T133732
+LAST-MODIFIED:20160602T133732
+UID:wej2z68l9h
+SUMMARY:Test Event
+BEGIN:VALARM
+ACTION:EMAIL
+TRIGGER:-PT15M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;VALUE=DATE-TIME:20160608T000000Z
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $objectData = [
+ 'calendardata' => $calendarData,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+
+ $this->backend->expects($this->never())
+ ->method('insertReminder');
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateSingleEntryWithRepeat(): void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_REPEAT,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+
+ $calls = [
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429500, false],
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429620, true],
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429740, true],
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429860, true],
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429980, true]
+ ];
+ $this->backend->expects($this->exactly(count($calls)))
+ ->method('insertReminder')
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return 1;
+ });
+
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00'));
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateRecurringEntry(): void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_RECURRING,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+
+ $calls = [
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'de919af7429d3b5c11e8b9d289b411a6', 'EMAIL', true, 1467243900, false],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false]
+ ];
+ $this->backend->expects($this->exactly(count($calls)))
+ ->method('insertReminder')
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return 1;
+ });
+
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-29T00:00:00+00:00'));
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateEmpty():void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_NO_ALARM,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+
+ $this->backend->expects($this->never())
+ ->method('insertReminder');
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateAllDayWithNullTimezone(): void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_ALL_DAY,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00'));
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null,
+ ]);
+
+ // One hour before midnight relative to the server's time
+ $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00'))->getTimestamp();
+ $this->backend->expects(self::once())
+ ->method('insertReminder')
+ ->with(1337, 42, self::anything(), false, 1675468800, false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false);
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateAllDayWithBlankTimezone(): void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_ALL_DAY,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00'));
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => '',
+ ]);
+
+ // One hour before midnight relative to the server's time
+ $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00'))->getTimestamp();
+ $this->backend->expects(self::once())
+ ->method('insertReminder')
+ ->with(1337, 42, self::anything(), false, 1675468800, false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false);
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateAllDayWithTimezone(): void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_ALL_DAY,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00'));
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => self::PAGO_PAGO_VTIMEZONE_ICS,
+ ]);
+
+ // One hour before midnight relative to the timezone
+ $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00', new DateTimeZone('Pacific/Pago_Pago')))->getTimestamp();
+ $this->backend->expects(self::once())
+ ->method('insertReminder')
+ ->with(1337, 42, 'a163a056-ba26-44a2-8080-955f19611a8f', false, self::anything(), false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false);
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateRecurringEntryWithRepeat():void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_RECURRING_REPEAT,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null,
+ ]);
+
+ $calls = [
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467243900, false],
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244020, true],
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244140, true],
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244260, true],
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244380, true],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false]
+ ];
+ $this->backend->expects($this->exactly(count($calls)))
+ ->method('insertReminder')
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return 1;
+ });
+
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-29T00:00:00+00:00'));
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateWithEventTimezoneAndCalendarTimezone():void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_ONE_TIME,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => self::PAGO_PAGO_VTIMEZONE_ICS,
+ ]);
+ $expectedReminderTimstamp = (new DateTime('2023-02-04T08:00:00', new DateTimeZone('Europe/Vienna')))->getTimestamp();
+ $this->backend->expects(self::once())
+ ->method('insertReminder')
+ ->with(1337, 42, self::anything(), false, self::anything(), false, self::anything(), self::anything(), self::anything(), true, $expectedReminderTimstamp, false)
+ ->willReturn(1);
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null,
+ ]);
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00'));
+ ;
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testProcessReminders():void {
+ $this->backend->expects($this->once())
+ ->method('getRemindersToProcess')
+ ->with()
+ ->willReturn([
+ [
+ 'id' => 1,
+ 'calendar_id' => 1337,
+ 'object_id' => 42,
+ 'uid' => 'wej2z68l9h',
+ 'is_recurring' => false,
+ 'recurrence_id' => 1465430400,
+ 'is_recurrence_exception' => false,
+ 'event_hash' => '5c70531aab15c92b52518ae10a2f78a4',
+ 'alarm_hash' => 'de919af7429d3b5c11e8b9d289b411a6',
+ 'type' => 'EMAIL',
+ 'is_relative' => true,
+ 'notification_date' => 1465429500,
+ 'is_repeat_based' => false,
+ 'calendardata' => self::CALENDAR_DATA,
+ 'displayname' => 'Displayname 123',
+ 'principaluri' => 'principals/users/user001',
+ ],
+ [
+ 'id' => 2,
+ 'calendar_id' => 1337,
+ 'object_id' => 42,
+ 'uid' => 'wej2z68l9h',
+ 'is_recurring' => false,
+ 'recurrence_id' => 1465430400,
+ 'is_recurrence_exception' => false,
+ 'event_hash' => '5c70531aab15c92b52518ae10a2f78a4',
+ 'alarm_hash' => 'ecacbf07d413c3c78d1ac7ad8c469602',
+ 'type' => 'EMAIL',
+ 'is_relative' => true,
+ 'notification_date' => 1465429740,
+ 'is_repeat_based' => true,
+ 'calendardata' => self::CALENDAR_DATA_REPEAT,
+ 'displayname' => 'Displayname 123',
+ 'principaluri' => 'principals/users/user001',
+ ],
+ [
+ 'id' => 3,
+ 'calendar_id' => 1337,
+ 'object_id' => 42,
+ 'uid' => 'wej2z68l9h',
+ 'is_recurring' => false,
+ 'recurrence_id' => 1465430400,
+ 'is_recurrence_exception' => false,
+ 'event_hash' => '5c70531aab15c92b52518ae10a2f78a4',
+ 'alarm_hash' => '35b3eae8e792aa2209f0b4e1a302f105',
+ 'type' => 'DISPLAY',
+ 'is_relative' => false,
+ 'notification_date' => 1465344000,
+ 'is_repeat_based' => false,
+ 'calendardata' => self::CALENDAR_DATA,
+ 'displayname' => 'Displayname 123',
+ 'principaluri' => 'principals/users/user001',
+ ],
+ [
+ 'id' => 4,
+ 'calendar_id' => 1337,
+ 'object_id' => 42,
+ 'uid' => 'wej2z68l9h',
+ 'is_recurring' => true,
+ 'recurrence_id' => 1467244800,
+ 'is_recurrence_exception' => false,
+ 'event_hash' => 'fbdb2726bc0f7dfacac1d881c1453e20',
+ 'alarm_hash' => 'ecacbf07d413c3c78d1ac7ad8c469602',
+ 'type' => 'EMAIL',
+ 'is_relative' => true,
+ 'notification_date' => 1467243900,
+ 'is_repeat_based' => false,
+ 'calendardata' => self::CALENDAR_DATA_RECURRING_REPEAT,
+ 'displayname' => 'Displayname 123',
+ 'principaluri' => 'principals/users/user001',
+ ],
+ [
+ 'id' => 5,
+ 'calendar_id' => 1337,
+ 'object_id' => 42,
+ 'uid' => 'wej2z68l9h',
+ 'is_recurring' => true,
+ 'recurrence_id' => 1467849600,
+ 'is_recurrence_exception' => false,
+ 'event_hash' => 'fbdb2726bc0f7dfacac1d881c1453e20',
+ 'alarm_hash' => '8996992118817f9f311ac5cc56d1cc97',
+ 'type' => 'EMAIL',
+ 'is_relative' => true,
+ 'notification_date' => 1467158400,
+ 'is_repeat_based' => false,
+ 'calendardata' => self::CALENDAR_DATA_RECURRING,
+ 'displayname' => 'Displayname 123',
+ 'principaluri' => 'principals/users/user001',
+ ]
+ ]);
+
+ $this->notificationProviderManager->expects($this->exactly(5))
+ ->method('hasProvider')
+ ->willReturnMap([
+ ['EMAIL', true],
+ ['DISPLAY', true],
+ ]);
+
+ $provider1 = $this->createMock(INotificationProvider::class);
+ $provider2 = $this->createMock(INotificationProvider::class);
+ $provider3 = $this->createMock(INotificationProvider::class);
+ $provider4 = $this->createMock(INotificationProvider::class);
+ $provider5 = $this->createMock(INotificationProvider::class);
+
+ $getProviderCalls = [
+ ['EMAIL', $provider1],
+ ['EMAIL', $provider2],
+ ['DISPLAY', $provider3],
+ ['EMAIL', $provider4],
+ ['EMAIL', $provider5],
+ ];
+ $this->notificationProviderManager->expects($this->exactly(count($getProviderCalls)))
+ ->method('getProvider')
+ ->willReturnCallback(function () use (&$getProviderCalls) {
+ $expected = array_shift($getProviderCalls);
+ $return = array_pop($expected);
+ $this->assertEquals($expected, func_get_args());
+ return $return;
+ });
+
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects($this->exactly(5))
+ ->method('get')
+ ->with('user001')
+ ->willReturn($user);
+
+ $provider1->expects($this->once())
+ ->method('send')
+ ->with($this->callback(function ($vevent) {
+ if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') {
+ return false;
+ }
+ return true;
+ }, 'Displayname 123', $user));
+ $provider2->expects($this->once())
+ ->method('send')
+ ->with($this->callback(function ($vevent) {
+ if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') {
+ return false;
+ }
+ return true;
+ }, 'Displayname 123', $user));
+ $provider3->expects($this->once())
+ ->method('send')
+ ->with($this->callback(function ($vevent) {
+ if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') {
+ return false;
+ }
+ return true;
+ }, 'Displayname 123', $user));
+ $provider4->expects($this->once())
+ ->method('send')
+ ->with($this->callback(function ($vevent) {
+ if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-30T00:00:00+00:00') {
+ return false;
+ }
+ return true;
+ }, 'Displayname 123', $user));
+ $provider5->expects($this->once())
+ ->method('send')
+ ->with($this->callback(function ($vevent) {
+ if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-07-07T00:00:00+00:00') {
+ return false;
+ }
+ return true;
+ }, 'Displayname 123', $user));
+
+ $removeReminderCalls = [
+ [1],
+ [2],
+ [3],
+ [4],
+ [5],
+ ];
+ $this->backend->expects($this->exactly(5))
+ ->method('removeReminder')
+ ->willReturnCallback(function () use (&$removeReminderCalls): void {
+ $expected = array_shift($removeReminderCalls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+
+ $insertReminderCalls = [
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848700, false],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848820, true],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848940, true],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467849060, true],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467849180, true],
+ [1337, 42, 'wej2z68l9h', true, 1468454400, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467763200, false],
+ ];
+ $this->backend->expects($this->exactly(count($insertReminderCalls)))
+ ->method('insertReminder')
+ ->willReturnCallback(function () use (&$insertReminderCalls) {
+ $expected = array_shift($insertReminderCalls);
+ $this->assertEquals($expected, func_get_args());
+ return 99;
+ });
+
+ $this->timeFactory->method('getDateTime')
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00'));
+
+ $this->reminderService->processReminders();
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTestCase.php b/apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTestCase.php
new file mode 100644
index 00000000000..364bc74de49
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTestCase.php
@@ -0,0 +1,556 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\ResourceBooking;
+
+use OCA\DAV\CalDAV\Proxy\Proxy;
+use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\CalDAV\ResourceBooking\ResourcePrincipalBackend;
+use OCA\DAV\CalDAV\ResourceBooking\RoomPrincipalBackend;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\PropPatch;
+use Test\TestCase;
+
+abstract class AbstractPrincipalBackendTestCase extends TestCase {
+ protected ResourcePrincipalBackend|RoomPrincipalBackend $principalBackend;
+ protected IUserSession&MockObject $userSession;
+ protected IGroupManager&MockObject $groupManager;
+ protected LoggerInterface&MockObject $logger;
+ protected ProxyMapper&MockObject $proxyMapper;
+ protected string $mainDbTable;
+ protected string $metadataDbTable;
+ protected string $foreignKey;
+ protected string $principalPrefix;
+ protected string $expectedCUType;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->proxyMapper = $this->createMock(ProxyMapper::class);
+ }
+
+ protected function tearDown(): void {
+ $query = self::$realDatabase->getQueryBuilder();
+
+ $query->delete('calendar_resources')->executeStatement();
+ $query->delete('calendar_resources_md')->executeStatement();
+ $query->delete('calendar_rooms')->executeStatement();
+ $query->delete('calendar_rooms_md')->executeStatement();
+ }
+
+ public function testGetPrincipalsByPrefix(): void {
+ $actual = $this->principalBackend->getPrincipalsByPrefix($this->principalPrefix);
+
+ $this->assertEquals([
+ [
+ 'uri' => $this->principalPrefix . '/backend1-res1',
+ '{DAV:}displayname' => 'Beamer1',
+ '{http://sabredav.org/ns}email-address' => 'res1@foo.bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType,
+ ],
+ [
+ 'uri' => $this->principalPrefix . '/backend1-res2',
+ '{DAV:}displayname' => 'TV1',
+ '{http://sabredav.org/ns}email-address' => 'res2@foo.bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType,
+ ],
+ [
+ 'uri' => $this->principalPrefix . '/backend2-res3',
+ '{DAV:}displayname' => 'Beamer2',
+ '{http://sabredav.org/ns}email-address' => 'res3@foo.bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType,
+ '{http://nextcloud.com/ns}foo' => 'value1',
+ '{http://nextcloud.com/ns}meta2' => 'value2',
+ ],
+ [
+ 'uri' => $this->principalPrefix . '/backend2-res4',
+ '{DAV:}displayname' => 'TV2',
+ '{http://sabredav.org/ns}email-address' => 'res4@foo.bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType,
+ '{http://nextcloud.com/ns}meta1' => 'value1',
+ '{http://nextcloud.com/ns}meta3' => 'value3-old',
+ ],
+ [
+ 'uri' => $this->principalPrefix . '/backend3-res5',
+ '{DAV:}displayname' => 'Beamer3',
+ '{http://sabredav.org/ns}email-address' => 'res5@foo.bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType,
+ ],
+ [
+ 'uri' => $this->principalPrefix . '/backend3-res6',
+ '{DAV:}displayname' => 'Pointer',
+ '{http://sabredav.org/ns}email-address' => 'res6@foo.bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType,
+ '{http://nextcloud.com/ns}meta99' => 'value99'
+ ]
+ ], $actual);
+ }
+
+ public function testGetNoPrincipalsByPrefixForWrongPrincipalPrefix(): void {
+ $actual = $this->principalBackend->getPrincipalsByPrefix('principals/users');
+ $this->assertEquals([], $actual);
+ }
+
+ public function testGetPrincipalByPath(): void {
+ $actual = $this->principalBackend->getPrincipalByPath($this->principalPrefix . '/backend2-res3');
+ $this->assertEquals([
+ 'uri' => $this->principalPrefix . '/backend2-res3',
+ '{DAV:}displayname' => 'Beamer2',
+ '{http://sabredav.org/ns}email-address' => 'res3@foo.bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType,
+ '{http://nextcloud.com/ns}foo' => 'value1',
+ '{http://nextcloud.com/ns}meta2' => 'value2',
+ ], $actual);
+ }
+
+ public function testGetPrincipalByPathNotFound(): void {
+ $actual = $this->principalBackend->getPrincipalByPath($this->principalPrefix . '/db-123');
+ $this->assertEquals(null, $actual);
+ }
+
+ public function testGetPrincipalByPathWrongPrefix(): void {
+ $actual = $this->principalBackend->getPrincipalByPath('principals/users/foo-bar');
+ $this->assertEquals(null, $actual);
+ }
+
+ public function testGetGroupMemberSet(): void {
+ $actual = $this->principalBackend->getGroupMemberSet($this->principalPrefix . '/backend1-res1');
+ $this->assertEquals([], $actual);
+ }
+
+ public function testGetGroupMemberSetProxyRead(): void {
+ $proxy1 = new Proxy();
+ $proxy1->setProxyId('proxyId1');
+ $proxy1->setPermissions(1);
+
+ $proxy2 = new Proxy();
+ $proxy2->setProxyId('proxyId2');
+ $proxy2->setPermissions(3);
+
+ $proxy3 = new Proxy();
+ $proxy3->setProxyId('proxyId3');
+ $proxy3->setPermissions(3);
+
+ $this->proxyMapper->expects($this->once())
+ ->method('getProxiesOf')
+ ->with($this->principalPrefix . '/backend1-res1')
+ ->willReturn([$proxy1, $proxy2, $proxy3]);
+
+ $actual = $this->principalBackend->getGroupMemberSet($this->principalPrefix . '/backend1-res1/calendar-proxy-read');
+ $this->assertEquals(['proxyId1'], $actual);
+ }
+
+ public function testGetGroupMemberSetProxyWrite(): void {
+ $proxy1 = new Proxy();
+ $proxy1->setProxyId('proxyId1');
+ $proxy1->setPermissions(1);
+
+ $proxy2 = new Proxy();
+ $proxy2->setProxyId('proxyId2');
+ $proxy2->setPermissions(3);
+
+ $proxy3 = new Proxy();
+ $proxy3->setProxyId('proxyId3');
+ $proxy3->setPermissions(3);
+
+ $this->proxyMapper->expects($this->once())
+ ->method('getProxiesOf')
+ ->with($this->principalPrefix . '/backend1-res1')
+ ->willReturn([$proxy1, $proxy2, $proxy3]);
+
+ $actual = $this->principalBackend->getGroupMemberSet($this->principalPrefix . '/backend1-res1/calendar-proxy-write');
+ $this->assertEquals(['proxyId2', 'proxyId3'], $actual);
+ }
+
+ public function testGetGroupMembership(): void {
+ $proxy1 = new Proxy();
+ $proxy1->setOwnerId('proxyId1');
+ $proxy1->setPermissions(1);
+
+ $proxy2 = new Proxy();
+ $proxy2->setOwnerId('proxyId2');
+ $proxy2->setPermissions(3);
+
+ $this->proxyMapper->expects($this->once())
+ ->method('getProxiesFor')
+ ->with($this->principalPrefix . '/backend1-res1')
+ ->willReturn([$proxy1, $proxy2]);
+
+ $actual = $this->principalBackend->getGroupMembership($this->principalPrefix . '/backend1-res1');
+
+ $this->assertEquals(['proxyId1/calendar-proxy-read', 'proxyId2/calendar-proxy-write'], $actual);
+ }
+
+ public function testSetGroupMemberSet(): void {
+ $this->proxyMapper->expects($this->once())
+ ->method('getProxiesOf')
+ ->with($this->principalPrefix . '/backend1-res1')
+ ->willReturn([]);
+
+ $calls = [
+ function ($proxy) {
+ /** @var Proxy $proxy */
+ if ($proxy->getOwnerId() !== $this->principalPrefix . '/backend1-res1') {
+ return false;
+ }
+ if ($proxy->getProxyId() !== $this->principalPrefix . '/backend1-res2') {
+ return false;
+ }
+ if ($proxy->getPermissions() !== 3) {
+ return false;
+ }
+
+ return true;
+ },
+ function ($proxy) {
+ /** @var Proxy $proxy */
+ if ($proxy->getOwnerId() !== $this->principalPrefix . '/backend1-res1') {
+ return false;
+ }
+ if ($proxy->getProxyId() !== $this->principalPrefix . '/backend2-res3') {
+ return false;
+ }
+ if ($proxy->getPermissions() !== 3) {
+ return false;
+ }
+
+ return true;
+ }
+ ];
+ $this->proxyMapper->expects($this->exactly(2))
+ ->method('insert')
+ ->willReturnCallback(function ($proxy) use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertTrue($expected($proxy));
+ return $proxy;
+ });
+
+ $this->principalBackend->setGroupMemberSet($this->principalPrefix . '/backend1-res1/calendar-proxy-write', [$this->principalPrefix . '/backend1-res2', $this->principalPrefix . '/backend2-res3']);
+ }
+
+ public function testUpdatePrincipal(): void {
+ $propPatch = $this->createMock(PropPatch::class);
+ $actual = $this->principalBackend->updatePrincipal($this->principalPrefix . '/foo-bar', $propPatch);
+
+ $this->assertEquals(0, $actual);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSearchPrincipals')]
+ public function testSearchPrincipals($expected, $test): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->with()
+ ->willReturn($user);
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2']);
+
+ $actual = $this->principalBackend->searchPrincipals($this->principalPrefix, [
+ '{http://sabredav.org/ns}email-address' => 'foo',
+ '{DAV:}displayname' => 'Beamer',
+ ], $test);
+
+ $this->assertEquals(
+ str_replace('%prefix%', $this->principalPrefix, $expected),
+ $actual);
+ }
+
+ public static function dataSearchPrincipals(): array {
+ // data providers are called before we subclass
+ // this class, $this->principalPrefix is null
+ // at that point, so we need this hack
+ return [
+ [[
+ '%prefix%/backend1-res1',
+ '%prefix%/backend2-res3',
+ ], 'allof'],
+ [[
+ '%prefix%/backend1-res1',
+ '%prefix%/backend1-res2',
+ '%prefix%/backend2-res3',
+ '%prefix%/backend2-res4',
+ '%prefix%/backend3-res6',
+ ], 'anyof'],
+ ];
+ }
+
+ public function testSearchPrincipalsByMetadataKey(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->with()
+ ->willReturn($user);
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2']);
+
+ $actual = $this->principalBackend->searchPrincipals($this->principalPrefix, [
+ '{http://nextcloud.com/ns}meta3' => 'value',
+ ]);
+
+ $this->assertEquals([
+ $this->principalPrefix . '/backend2-res4',
+ ], $actual);
+ }
+
+ public function testSearchPrincipalsByCalendarUserAddressSet(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->method('getUser')
+ ->with()
+ ->willReturn($user);
+ $this->groupManager->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2']);
+
+ $actual = $this->principalBackend->searchPrincipals($this->principalPrefix, [
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => 'res2@foo.bar',
+ ]);
+
+ $this->assertEquals(
+ str_replace('%prefix%', $this->principalPrefix, [
+ '%prefix%/backend1-res2',
+ ]),
+ $actual);
+ }
+
+ public function testSearchPrincipalsEmptySearchProperties(): void {
+ $this->userSession->expects($this->never())
+ ->method('getUser');
+ $this->groupManager->expects($this->never())
+ ->method('getUserGroupIds');
+
+ $this->principalBackend->searchPrincipals($this->principalPrefix, []);
+ }
+
+ public function testSearchPrincipalsWrongPrincipalPrefix(): void {
+ $this->userSession->expects($this->never())
+ ->method('getUser');
+ $this->groupManager->expects($this->never())
+ ->method('getUserGroupIds');
+
+ $this->principalBackend->searchPrincipals('principals/users', [
+ '{http://sabredav.org/ns}email-address' => 'foo'
+ ]);
+ }
+
+ public function testFindByUriByEmail(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->with()
+ ->willReturn($user);
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2']);
+
+ $actual = $this->principalBackend->findByUri('mailto:res1@foo.bar', $this->principalPrefix);
+ $this->assertEquals($this->principalPrefix . '/backend1-res1', $actual);
+ }
+
+ public function testFindByUriByEmailForbiddenResource(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->with()
+ ->willReturn($user);
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2']);
+
+ $actual = $this->principalBackend->findByUri('mailto:res5@foo.bar', $this->principalPrefix);
+ $this->assertEquals(null, $actual);
+ }
+
+ public function testFindByUriByEmailNotFound(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->with()
+ ->willReturn($user);
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2']);
+
+ $actual = $this->principalBackend->findByUri('mailto:res99@foo.bar', $this->principalPrefix);
+ $this->assertEquals(null, $actual);
+ }
+
+ public function testFindByUriByPrincipal(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->with()
+ ->willReturn($user);
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2']);
+
+ $actual = $this->principalBackend->findByUri('mailto:res6@foo.bar', $this->principalPrefix);
+ $this->assertEquals($this->principalPrefix . '/backend3-res6', $actual);
+ }
+
+ public function testFindByUriByPrincipalForbiddenResource(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->with()
+ ->willReturn($user);
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2']);
+
+ $actual = $this->principalBackend->findByUri('principal:' . $this->principalPrefix . '/backend3-res5', $this->principalPrefix);
+ $this->assertEquals(null, $actual);
+ }
+
+ public function testFindByUriByPrincipalNotFound(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->with()
+ ->willReturn($user);
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2']);
+
+ $actual = $this->principalBackend->findByUri('principal:' . $this->principalPrefix . '/db-123', $this->principalPrefix);
+ $this->assertEquals(null, $actual);
+ }
+
+ public function testFindByUriByUnknownUri(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->with()
+ ->willReturn($user);
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2']);
+
+ $actual = $this->principalBackend->findByUri('foobar:blub', $this->principalPrefix);
+ $this->assertEquals(null, $actual);
+ }
+
+ protected function createTestDatasetInDb() {
+ $query = self::$realDatabase->getQueryBuilder();
+ $query->insert($this->mainDbTable)
+ ->values([
+ 'backend_id' => $query->createNamedParameter('backend1'),
+ 'resource_id' => $query->createNamedParameter('res1'),
+ 'email' => $query->createNamedParameter('res1@foo.bar'),
+ 'displayname' => $query->createNamedParameter('Beamer1'),
+ 'group_restrictions' => $query->createNamedParameter('[]'),
+ ])
+ ->execute();
+
+ $query->insert($this->mainDbTable)
+ ->values([
+ 'backend_id' => $query->createNamedParameter('backend1'),
+ 'resource_id' => $query->createNamedParameter('res2'),
+ 'email' => $query->createNamedParameter('res2@foo.bar'),
+ 'displayname' => $query->createNamedParameter('TV1'),
+ 'group_restrictions' => $query->createNamedParameter('[]'),
+ ])
+ ->execute();
+
+ $query->insert($this->mainDbTable)
+ ->values([
+ 'backend_id' => $query->createNamedParameter('backend2'),
+ 'resource_id' => $query->createNamedParameter('res3'),
+ 'email' => $query->createNamedParameter('res3@foo.bar'),
+ 'displayname' => $query->createNamedParameter('Beamer2'),
+ 'group_restrictions' => $query->createNamedParameter('[]'),
+ ])
+ ->execute();
+ $id3 = $query->getLastInsertId();
+
+ $query->insert($this->mainDbTable)
+ ->values([
+ 'backend_id' => $query->createNamedParameter('backend2'),
+ 'resource_id' => $query->createNamedParameter('res4'),
+ 'email' => $query->createNamedParameter('res4@foo.bar'),
+ 'displayname' => $query->createNamedParameter('TV2'),
+ 'group_restrictions' => $query->createNamedParameter('[]'),
+ ])
+ ->execute();
+ $id4 = $query->getLastInsertId();
+
+ $query->insert($this->mainDbTable)
+ ->values([
+ 'backend_id' => $query->createNamedParameter('backend3'),
+ 'resource_id' => $query->createNamedParameter('res5'),
+ 'email' => $query->createNamedParameter('res5@foo.bar'),
+ 'displayname' => $query->createNamedParameter('Beamer3'),
+ 'group_restrictions' => $query->createNamedParameter('["foo", "bar"]'),
+ ])
+ ->execute();
+
+ $query->insert($this->mainDbTable)
+ ->values([
+ 'backend_id' => $query->createNamedParameter('backend3'),
+ 'resource_id' => $query->createNamedParameter('res6'),
+ 'email' => $query->createNamedParameter('res6@foo.bar'),
+ 'displayname' => $query->createNamedParameter('Pointer'),
+ 'group_restrictions' => $query->createNamedParameter('["group1", "bar"]'),
+ ])
+ ->execute();
+ $id6 = $query->getLastInsertId();
+
+ $query->insert($this->metadataDbTable)
+ ->values([
+ $this->foreignKey => $query->createNamedParameter($id3),
+ 'key' => $query->createNamedParameter('{http://nextcloud.com/ns}foo'),
+ 'value' => $query->createNamedParameter('value1')
+ ])
+ ->execute();
+ $query->insert($this->metadataDbTable)
+ ->values([
+ $this->foreignKey => $query->createNamedParameter($id3),
+ 'key' => $query->createNamedParameter('{http://nextcloud.com/ns}meta2'),
+ 'value' => $query->createNamedParameter('value2')
+ ])
+ ->execute();
+ $query->insert($this->metadataDbTable)
+ ->values([
+ $this->foreignKey => $query->createNamedParameter($id4),
+ 'key' => $query->createNamedParameter('{http://nextcloud.com/ns}meta1'),
+ 'value' => $query->createNamedParameter('value1')
+ ])
+ ->execute();
+ $query->insert($this->metadataDbTable)
+ ->values([
+ $this->foreignKey => $query->createNamedParameter($id4),
+ 'key' => $query->createNamedParameter('{http://nextcloud.com/ns}meta3'),
+ 'value' => $query->createNamedParameter('value3-old')
+ ])
+ ->execute();
+ $query->insert($this->metadataDbTable)
+ ->values([
+ $this->foreignKey => $query->createNamedParameter($id6),
+ 'key' => $query->createNamedParameter('{http://nextcloud.com/ns}meta99'),
+ 'value' => $query->createNamedParameter('value99')
+ ])
+ ->execute();
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php b/apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php
new file mode 100644
index 00000000000..168e21c3a91
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\ResourceBooking;
+
+use OCA\DAV\CalDAV\ResourceBooking\ResourcePrincipalBackend;
+
+class ResourcePrincipalBackendTest extends AbstractPrincipalBackendTestCase {
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->principalBackend = new ResourcePrincipalBackend(self::$realDatabase,
+ $this->userSession, $this->groupManager, $this->logger, $this->proxyMapper);
+
+ $this->mainDbTable = 'calendar_resources';
+ $this->metadataDbTable = 'calendar_resources_md';
+ $this->foreignKey = 'resource_id';
+
+ $this->principalPrefix = 'principals/calendar-resources';
+ $this->expectedCUType = 'RESOURCE';
+
+ $this->createTestDatasetInDb();
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php b/apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php
new file mode 100644
index 00000000000..8a53b0ee25e
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\ResourceBooking;
+
+use OCA\DAV\CalDAV\ResourceBooking\RoomPrincipalBackend;
+
+class RoomPrincipalBackendTest extends AbstractPrincipalBackendTestCase {
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->principalBackend = new RoomPrincipalBackend(self::$realDatabase,
+ $this->userSession, $this->groupManager, $this->logger, $this->proxyMapper);
+
+ $this->mainDbTable = 'calendar_rooms';
+ $this->metadataDbTable = 'calendar_rooms_md';
+ $this->foreignKey = 'room_id';
+
+ $this->principalPrefix = 'principals/calendar-rooms';
+ $this->expectedCUType = 'ROOM';
+
+ $this->createTestDatasetInDb();
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php
new file mode 100644
index 00000000000..fa52d5319c9
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php
@@ -0,0 +1,193 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV\Schedule;
+
+use OC\L10N\L10N;
+use OC\URLGenerator;
+use OCA\DAV\CalDAV\EventComparisonService;
+use OCA\DAV\CalDAV\Schedule\IMipPlugin;
+use OCA\DAV\CalDAV\Schedule\IMipService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Defaults;
+use OCP\IAppConfig;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Mail\IMailer;
+use OCP\Mail\IMessage;
+use OCP\Mail\Provider\IManager;
+use OCP\Mail\Provider\IMessageSend;
+use OCP\Mail\Provider\IService;
+use OCP\Mail\Provider\Message as MailProviderMessage;
+use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\ITip\Message;
+use Sabre\VObject\Property\ICalendar\CalAddress;
+use Symfony\Component\Mime\Email;
+use Test\TestCase;
+
+class IMipPluginCharsetTest extends TestCase {
+ // Dependencies
+ private Defaults&MockObject $defaults;
+ private IAppConfig&MockObject $appConfig;
+ private IConfig&MockObject $config;
+ private IDBConnection&MockObject $db;
+ private IFactory $l10nFactory;
+ private IManager&MockObject $mailManager;
+ private IMailer&MockObject $mailer;
+ private ISecureRandom&MockObject $random;
+ private ITimeFactory&MockObject $timeFactory;
+ private IUrlGenerator&MockObject $urlGenerator;
+ private IUserSession&MockObject $userSession;
+ private LoggerInterface $logger;
+
+ // Services
+ private EventComparisonService $eventComparisonService;
+ private IMipPlugin $imipPlugin;
+ private IMipService $imipService;
+
+ // ITip Message
+ private Message $itipMessage;
+
+ protected function setUp(): void {
+ // Used by IMipService and IMipPlugin
+ $today = new \DateTime('2025-06-15 14:30');
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->timeFactory->method('getTime')
+ ->willReturn($today->getTimestamp());
+ $this->timeFactory->method('getDateTime')
+ ->willReturn($today);
+
+ // IMipService
+ $this->urlGenerator = $this->createMock(URLGenerator::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->db = $this->createMock(IDBConnection::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $l10n = $this->createMock(L10N::class);
+ $this->l10nFactory = $this->createMock(IFactory::class);
+ $this->l10nFactory->method('findGenericLanguage')
+ ->willReturn('en');
+ $this->l10nFactory->method('findLocale')
+ ->willReturn('en_US');
+ $this->l10nFactory->method('get')
+ ->willReturn($l10n);
+ $this->imipService = new IMipService(
+ $this->urlGenerator,
+ $this->config,
+ $this->db,
+ $this->random,
+ $this->l10nFactory,
+ $this->timeFactory,
+ );
+
+ // EventComparisonService
+ $this->eventComparisonService = new EventComparisonService();
+
+ // IMipPlugin
+ $this->appConfig = $this->createMock(IAppConfig::class);
+ $message = new \OC\Mail\Message(new Email(), false);
+ $this->mailer = $this->createMock(IMailer::class);
+ $this->mailer->method('createMessage')
+ ->willReturn($message);
+ $this->mailer->method('validateMailAddress')
+ ->willReturn(true);
+ $this->logger = new NullLogger();
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->defaults->method('getName')
+ ->willReturn('Instance Name 123');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('luigi');
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->userSession->method('getUser')
+ ->willReturn($user);
+ $this->mailManager = $this->createMock(IManager::class);
+ $this->imipPlugin = new IMipPlugin(
+ $this->appConfig,
+ $this->mailer,
+ $this->logger,
+ $this->timeFactory,
+ $this->defaults,
+ $this->userSession,
+ $this->imipService,
+ $this->eventComparisonService,
+ $this->mailManager,
+ );
+
+ // ITipMessage
+ $calendar = new VCalendar();
+ $event = new VEvent($calendar, 'VEVENT');
+ $event->UID = 'uid-1234';
+ $event->SEQUENCE = 1;
+ $event->SUMMARY = 'Lunch';
+ $event->DTSTART = new \DateTime('2025-06-20 12:30:00');
+ $organizer = new CalAddress($calendar, 'ORGANIZER', 'mailto:luigi@example.org');
+ $event->add($organizer);
+ $attendee = new CalAddress($calendar, 'ATTENDEE', 'mailto:jose@example.org', ['RSVP' => 'TRUE', 'CN' => 'José']);
+ $event->add($attendee);
+ $calendar->add($event);
+ $this->itipMessage = new Message();
+ $this->itipMessage->method = 'REQUEST';
+ $this->itipMessage->message = $calendar;
+ $this->itipMessage->sender = 'mailto:luigi@example.org';
+ $this->itipMessage->senderName = 'Luigi';
+ $this->itipMessage->recipient = 'mailto:' . 'jose@example.org';
+ }
+
+ public function testCharsetMailer(): void {
+ // Arrange
+ $symfonyEmail = null;
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturnCallback(function (IMessage $message) use (&$symfonyEmail): array {
+ if ($message instanceof \OC\Mail\Message) {
+ $symfonyEmail = $message->getSymfonyEmail();
+ }
+ return [];
+ });
+
+ // Act
+ $this->imipPlugin->schedule($this->itipMessage);
+
+ // Assert
+ $this->assertNotNull($symfonyEmail);
+ $body = $symfonyEmail->getBody()->toString();
+ $this->assertStringContainsString('Content-Type: text/calendar; method=REQUEST; charset="utf-8"; name=event.ics', $body);
+ }
+
+ public function testCharsetMailProvider(): void {
+ // Arrange
+ $this->appConfig->method('getValueBool')
+ ->with('core', 'mail_providers_enabled', true)
+ ->willReturn(true);
+ $mailMessage = new MailProviderMessage();
+ $mailService = $this->createStubForIntersectionOfInterfaces([IService::class, IMessageSend::class]);
+ $mailService->method('initiateMessage')
+ ->willReturn($mailMessage);
+ $mailService->expects(self::once())
+ ->method('sendMessage');
+ $this->mailManager->method('findServiceByAddress')
+ ->willReturn($mailService);
+
+ // Act
+ $this->imipPlugin->schedule($this->itipMessage);
+
+ // Assert
+ $attachments = $mailMessage->getAttachments();
+ $this->assertCount(1, $attachments);
+ $this->assertStringContainsString('text/calendar; method=REQUEST; charset="utf-8"; name=event.ics', $attachments[0]->getType());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
new file mode 100644
index 00000000000..8e71bfa6edf
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
@@ -0,0 +1,1080 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Schedule;
+
+use OCA\DAV\CalDAV\EventComparisonService;
+use OCA\DAV\CalDAV\Schedule\IMipPlugin;
+use OCA\DAV\CalDAV\Schedule\IMipService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Defaults;
+use OCP\IAppConfig;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Mail\IAttachment;
+use OCP\Mail\IEMailTemplate;
+use OCP\Mail\IMailer;
+use OCP\Mail\IMessage;
+use OCP\Mail\Provider\IManager as IMailManager;
+use OCP\Mail\Provider\IMessage as IMailMessageNew;
+use OCP\Mail\Provider\IMessageSend as IMailMessageSend;
+use OCP\Mail\Provider\IService as IMailService;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\ITip\Message;
+use Test\TestCase;
+use function array_merge;
+
+interface IMailServiceMock extends IMailService, IMailMessageSend {
+ // workaround for creating mock class with multiple interfaces
+ // TODO: remove after phpUnit 10 is supported.
+}
+
+class IMipPluginTest extends TestCase {
+ private IMessage&MockObject $mailMessage;
+ private IMailer&MockObject $mailer;
+ private IEMailTemplate&MockObject $emailTemplate;
+ private IAttachment&MockObject $emailAttachment;
+ private ITimeFactory&MockObject $timeFactory;
+ private IAppConfig&MockObject $config;
+ private IUserSession&MockObject $userSession;
+ private IUser&MockObject $user;
+ private IMipPlugin $plugin;
+ private IMipService&MockObject $service;
+ private Defaults&MockObject $defaults;
+ private LoggerInterface&MockObject $logger;
+ private EventComparisonService&MockObject $eventComparisonService;
+ private IMailManager&MockObject $mailManager;
+ private IMailServiceMock&MockObject $mailService;
+ private IMailMessageNew&MockObject $mailMessageNew;
+
+ protected function setUp(): void {
+ $this->mailMessage = $this->createMock(IMessage::class);
+ $this->mailMessage->method('setFrom')->willReturn($this->mailMessage);
+ $this->mailMessage->method('setReplyTo')->willReturn($this->mailMessage);
+ $this->mailMessage->method('setTo')->willReturn($this->mailMessage);
+
+ $this->mailer = $this->createMock(IMailer::class);
+ $this->mailer->method('createMessage')->willReturn($this->mailMessage);
+
+ $this->emailTemplate = $this->createMock(IEMailTemplate::class);
+ $this->mailer->method('createEMailTemplate')->willReturn($this->emailTemplate);
+
+ $this->emailAttachment = $this->createMock(IAttachment::class);
+ $this->mailer->method('createAttachment')->willReturn($this->emailAttachment);
+
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->timeFactory->method('getTime')->willReturn(1496912528); // 2017-01-01
+
+ $this->config = $this->createMock(IAppConfig::class);
+
+ $this->user = $this->createMock(IUser::class);
+
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->userSession->method('getUser')
+ ->willReturn($this->user);
+
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->defaults->method('getName')
+ ->willReturn('Instance Name 123');
+
+ $this->service = $this->createMock(IMipService::class);
+
+ $this->eventComparisonService = $this->createMock(EventComparisonService::class);
+
+ $this->mailManager = $this->createMock(IMailManager::class);
+
+ $this->mailService = $this->createMock(IMailServiceMock::class);
+
+ $this->mailMessageNew = $this->createMock(IMailMessageNew::class);
+
+ $this->plugin = new IMipPlugin(
+ $this->config,
+ $this->mailer,
+ $this->logger,
+ $this->timeFactory,
+ $this->defaults,
+ $this->userSession,
+ $this->service,
+ $this->eventComparisonService,
+ $this->mailManager,
+ );
+ }
+
+ public function testDeliveryNoSignificantChange(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $message->message = new VCalendar();
+ $message->message->add('VEVENT', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $message->message->VEVENT->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE']);
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ $message->significantChange = false;
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.0', $message->getScheduleStatus());
+ }
+
+ public function testParsingSingle(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ // save the old copy in the plugin
+ $oldVCalendar = new VCalendar();
+ $oldVEvent = new VEvent($oldVCalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ]);
+ $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']);
+ $oldVCalendar->add($oldVEvent);
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting without (!) Boromir',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
+ }
+ $this->plugin->setVCalendar($oldVCalendar);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, $oldVEvent)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', true);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
+ ->willReturn('yes');
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $newVevent, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.1', $message->getScheduleStatus());
+ }
+
+ public function testAttendeeIsResource(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'the-shire@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'The Shire', 'CUTYPE' => 'ROOM']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'the-shire@hobb.it';
+ // save the old copy in the plugin
+ $oldVCalendar = new VCalendar();
+ $oldVEvent = new VEvent($oldVCalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ]);
+ $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'the-shire@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'The Shire', 'CUTYPE' => 'ROOM']);
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']);
+ $oldVCalendar->add($oldVEvent);
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting without (!) Boromir',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $room = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $room = $attendee;
+ }
+ }
+ $this->plugin->setVCalendar($oldVCalendar);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('the-shire@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($room);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($room)
+ ->willReturn(true);
+ $this->service->expects(self::never())
+ ->method('isCircle');
+ $this->service->expects(self::never())
+ ->method('buildBodyData');
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::never())
+ ->method('getFrom');
+ $this->service->expects(self::never())
+ ->method('addSubjectAndHeading');
+ $this->service->expects(self::never())
+ ->method('addBulletList');
+ $this->service->expects(self::never())
+ ->method('getAttendeeRsvpOrReqForParticipant');
+ $this->config->expects(self::never())
+ ->method('getValueString');
+ $this->service->expects(self::never())
+ ->method('createInvitationToken');
+ $this->service->expects(self::never())
+ ->method('addResponseButtons');
+ $this->service->expects(self::never())
+ ->method('addMoreOptionsButton');
+ $this->mailer->expects(self::never())
+ ->method('send');
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.0', $message->getScheduleStatus());
+ }
+
+ public function testAttendeeIsCircle(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth', ['RSVP' => 'TRUE', 'CN' => 'The Fellowship', 'CUTYPE' => 'GROUP']);
+ $newVevent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE', 'MEMBER' => 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth';
+ $attendees = $newVevent->select('ATTENDEE');
+ $circle = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $circle = $attendee;
+ }
+ }
+ $this->assertNotEmpty($circle, 'Failed to find attendee belonging to the circle');
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['new' => [$newVevent], 'old' => null]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($circle);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($circle)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($circle)
+ ->willReturn(true);
+ $this->service->expects(self::never())
+ ->method('buildBodyData');
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::never())
+ ->method('getFrom');
+ $this->service->expects(self::never())
+ ->method('addSubjectAndHeading');
+ $this->service->expects(self::never())
+ ->method('addBulletList');
+ $this->service->expects(self::never())
+ ->method('getAttendeeRsvpOrReqForParticipant');
+ $this->config->expects(self::never())
+ ->method('getValueString');
+ $this->service->expects(self::never())
+ ->method('createInvitationToken');
+ $this->service->expects(self::never())
+ ->method('addResponseButtons');
+ $this->service->expects(self::never())
+ ->method('addMoreOptionsButton');
+ $this->mailer->expects(self::never())
+ ->method('send');
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.0', $message->getScheduleStatus());
+ }
+
+ public function testParsingRecurrence(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z'
+ ]);
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $newvEvent2 = new VEvent($newVCalendar, 'two', [
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Elevenses',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00')
+ ]);
+ $newvEvent2->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newvEvent2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ // save the old copy in the plugin
+ $oldVCalendar = new VCalendar();
+ $oldVEvent = new VEvent($oldVCalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z'
+ ]);
+ $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Elevenses',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
+ }
+ $this->plugin->setVCalendar($oldVCalendar);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['old' => [] ,'new' => [$newVevent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, null)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Elevenses', false);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
+ ->willReturn('yes');
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $newVevent, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.1', $message->getScheduleStatus());
+ }
+
+ public function testEmailValidationFailed(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $message->message = new VCalendar();
+ $message->message->add('VEVENT', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $message->message->VEVENT->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE']);
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(false);
+
+ $this->plugin->schedule($message);
+ $this->assertEquals('5.0', $message->getScheduleStatus());
+ }
+
+ public function testFailedDelivery(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVcalendar = new VCalendar();
+ $newVevent = new VEvent($newVcalendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVcalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ // save the old copy in the plugin
+ $oldVcalendar = new VCalendar();
+ $oldVevent = new VEvent($oldVcalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ]);
+ $oldVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $oldVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $oldVevent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']);
+ $oldVcalendar->add($oldVevent);
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting without (!) Boromir',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
+ }
+ $this->plugin->setVCalendar($oldVcalendar);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['old' => [] ,'new' => [$newVevent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, null)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', false);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
+ ->willReturn('yes');
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $newVevent, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
+ $this->mailer
+ ->method('send')
+ ->willThrowException(new \Exception());
+ $this->logger->expects(self::once())
+ ->method('error');
+ $this->plugin->schedule($message);
+ $this->assertEquals('5.0', $message->getScheduleStatus());
+ }
+
+ public function testMailProviderSend(): void {
+ // construct iTip message with event and attendees
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $calendar = new VCalendar();
+ $event = new VEvent($calendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $event->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $event->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $calendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ // construct
+ foreach ($event->select('ATTENDEE') as $entry) {
+ if (strcasecmp($entry->getValue(), $message->recipient) === 0) {
+ $attendee = $entry;
+ }
+ }
+ // construct body data return
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting without (!) Boromir',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ // construct system config mock returns
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
+ ->willReturn('yes');
+ // construct user mock returns
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ // construct user session mock returns
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ // construct service mock returns
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($attendee);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($attendee)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($attendee)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($event, null)
+ ->willReturn($data);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', false);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $event, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $event, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['old' => [] ,'new' => [$event]]);
+ // construct mail mock returns
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ // construct mail provider mock returns
+ $this->mailService
+ ->method('initiateMessage')
+ ->willReturn($this->mailMessageNew);
+ $this->mailService
+ ->method('sendMessage')
+ ->with($this->mailMessageNew);
+ $this->mailManager
+ ->method('findServiceByAddress')
+ ->with('user1', 'gandalf@wiz.ard')
+ ->willReturn($this->mailService);
+
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.1', $message->getScheduleStatus());
+ }
+
+ public function testMailProviderDisabled(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ // save the old copy in the plugin
+ $oldVCalendar = new VCalendar();
+ $oldVEvent = new VEvent($oldVCalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ]);
+ $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']);
+ $oldVCalendar->add($oldVEvent);
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting without (!) Boromir',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
+ }
+ $this->plugin->setVCalendar($oldVCalendar);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, $oldVEvent)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', true);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
+ ->willReturn('yes');
+ $this->config->expects(self::once())
+ ->method('getValueBool')
+ ->with('core', 'mail_providers_enabled', true)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $newVevent, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.1', $message->getScheduleStatus());
+ }
+
+ public function testNoOldEvent(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'VEVENT', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
+ }
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->with($newVCalendar, null)
+ ->willReturn(['old' => [] ,'new' => [$newVevent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, null)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
+ ->willReturn('yes');
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $newVevent, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
+ $this->mailer
+ ->method('send')
+ ->willReturn([]);
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.1', $message->getScheduleStatus());
+ }
+
+ public function testNoButtons(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'VEVENT', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
+ }
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->with($newVCalendar, null)
+ ->willReturn(['old' => [] ,'new' => [$newVevent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, null)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
+ ->willReturn('no');
+ $this->service->expects(self::never())
+ ->method('createInvitationToken');
+ $this->service->expects(self::never())
+ ->method('addResponseButtons');
+ $this->service->expects(self::never())
+ ->method('addMoreOptionsButton');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
+ $this->mailer
+ ->method('send')
+ ->willReturn([]);
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.1', $message->getScheduleStatus());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php
new file mode 100644
index 00000000000..2be6a1cf8b1
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php
@@ -0,0 +1,2200 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV\Schedule;
+
+use OC\URLGenerator;
+use OCA\DAV\CalDAV\EventReader;
+use OCA\DAV\CalDAV\Schedule\IMipService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IL10N;
+use OCP\L10N\IFactory;
+use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Property\ICalendar\DateTime;
+use Test\TestCase;
+
+class IMipServiceTest extends TestCase {
+ private URLGenerator&MockObject $urlGenerator;
+ private IConfig&MockObject $config;
+ private IDBConnection&MockObject $db;
+ private ISecureRandom&MockObject $random;
+ private IFactory&MockObject $l10nFactory;
+ private IL10N&MockObject $l10n;
+ private ITimeFactory&MockObject $timeFactory;
+ private IMipService $service;
+
+
+ private VCalendar $vCalendar1a;
+ private VCalendar $vCalendar1b;
+ private VCalendar $vCalendar2;
+ private VCalendar $vCalendar3;
+ /** @var DateTime DateTime object that will be returned by DateTime() or DateTime('now') */
+ public static $datetimeNow;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->urlGenerator = $this->createMock(URLGenerator::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->db = $this->createMock(IDBConnection::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $this->l10nFactory = $this->createMock(IFactory::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->l10nFactory->expects(self::once())
+ ->method('findGenericLanguage')
+ ->willReturn('en');
+ $this->l10nFactory->expects(self::once())
+ ->method('get')
+ ->with('dav', 'en')
+ ->willReturn($this->l10n);
+ $this->service = new IMipService(
+ $this->urlGenerator,
+ $this->config,
+ $this->db,
+ $this->random,
+ $this->l10nFactory,
+ $this->timeFactory
+ );
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $this->vCalendar1a = new VCalendar();
+ $vEvent = $this->vCalendar1a->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and different start/end time zones
+ $this->vCalendar1b = new VCalendar();
+ $vEvent = $this->vCalendar1b->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Vancouver']);
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a full day event
+ $this->vCalendar2 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar2->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar2->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240702');
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a multi day event
+ $this->vCalendar3 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar3->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar3->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240706');
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ }
+
+ public function testGetFrom(): void {
+ $senderName = 'Detective McQueen';
+ $default = 'Twin Lakes Police Department - Darkside Division';
+ $expected = 'Detective McQueen via Twin Lakes Police Department - Darkside Division';
+
+ $this->l10n->expects(self::once())
+ ->method('t')
+ ->willReturn($expected);
+
+ $actual = $this->service->getFrom($senderName, $default);
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testBuildBodyDataCreated(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ [
+ 'In a day on %1$s between %2$s - %3$s',
+ 'In %n days on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ]
+ ]);
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnCallback(
+ function ($v1, $v2) {
+ return match (true) {
+ $v1 == 'now' && $v2 == null => (new \DateTime('20240630T000000'))
+ };
+ }
+ );
+ /** test singleton partial day event*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // define expected output
+ $expected = [
+ 'meeting_when' => $this->service->generateWhenString($eventReader),
+ 'meeting_description' => '',
+ 'meeting_title' => 'Testing Event',
+ 'meeting_location' => '',
+ 'meeting_url' => '',
+ 'meeting_url_html' => '',
+ ];
+ // generate actual output
+ $actual = $this->service->buildBodyData($vCalendar->VEVENT[0], null);
+ // test output
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testBuildBodyDataUpdate(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ [
+ 'In a day on %1$s between %2$s - %3$s',
+ 'In %n days on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ]
+ ]);
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnCallback(
+ function ($v1, $v2) {
+ return match (true) {
+ $v1 == 'now' && $v2 == null => (new \DateTime('20240630T000000'))
+ };
+ }
+ );
+ /** test singleton partial day event*/
+ $vCalendarNew = clone $this->vCalendar1a;
+ $vCalendarOld = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReaderNew = new EventReader($vCalendarNew, $vCalendarNew->VEVENT[0]->UID->getValue());
+ // alter old event label/title
+ $vCalendarOld->VEVENT[0]->SUMMARY->setValue('Testing Singleton Event');
+ // define expected output
+ $expected = [
+ 'meeting_when' => $this->service->generateWhenString($eventReaderNew),
+ 'meeting_description' => '',
+ 'meeting_title' => 'Testing Event',
+ 'meeting_location' => '',
+ 'meeting_url' => '',
+ 'meeting_url_html' => '',
+ 'meeting_when_html' => $this->service->generateWhenString($eventReaderNew),
+ 'meeting_title_html' => sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", 'Testing Singleton Event', 'Testing Event'),
+ 'meeting_description_html' => '',
+ 'meeting_location_html' => ''
+ ];
+ // generate actual output
+ $actual = $this->service->buildBodyData($vCalendarNew->VEVENT[0], $vCalendarOld->VEVENT[0]);
+ // test output
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testGetLastOccurrenceRRULE(): void {
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+
+ $occurrence = $this->service->getLastOccurrence($vCalendar);
+ $this->assertEquals(1454284800, $occurrence);
+ }
+
+ public function testGetLastOccurrenceEndDate(): void {
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'DTEND' => new \DateTime('2017-01-01 00:00:00'),
+ ]);
+
+ $occurrence = $this->service->getLastOccurrence($vCalendar);
+ $this->assertEquals(1483228800, $occurrence);
+ }
+
+ public function testGetLastOccurrenceDuration(): void {
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'DURATION' => 'P12W',
+ ]);
+
+ $occurrence = $this->service->getLastOccurrence($vCalendar);
+ $this->assertEquals(1458864000, $occurrence);
+ }
+
+ public function testGetLastOccurrenceAllDay(): void {
+ $vCalendar = new VCalendar();
+ $vEvent = $vCalendar->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+
+ // rewrite from DateTime to Date
+ $vEvent->DTSTART['VALUE'] = 'DATE';
+
+ $occurrence = $this->service->getLastOccurrence($vCalendar);
+ $this->assertEquals(1451692800, $occurrence);
+ }
+
+ public function testGetLastOccurrenceFallback(): void {
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+
+ $occurrence = $this->service->getLastOccurrence($vCalendar);
+ $this->assertEquals(1451606400, $occurrence);
+ }
+
+ public function testGenerateWhenStringSingular(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T000000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ [
+ 'In the past on %1$s for the entire day',
+ ['July 1, 2024'],
+ 'In the past on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In the past on %1$s between %2$s - %3$s',
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In the past on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ ]);
+ $this->l10n->method('n')->willReturnMap([
+ // singular entire day
+ [
+ 'In a minute on %1$s for the entire day',
+ 'In %n minutes on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a minute on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a hour on %1$s for the entire day',
+ 'In %n hours on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a hour on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a day on %1$s for the entire day',
+ 'In %n days on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a day on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a week on %1$s for the entire day',
+ 'In %n weeks on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a week on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a month on %1$s for the entire day',
+ 'In %n months on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a month on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a year on %1$s for the entire day',
+ 'In %n years on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a year on July 1, 2024 for the entire day'
+ ],
+ // plural entire day
+ [
+ 'In a minute on %1$s for the entire day',
+ 'In %n minutes on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 minutes on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a hour on %1$s for the entire day',
+ 'In %n hours on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 hours on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a day on %1$s for the entire day',
+ 'In %n days on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 days on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a week on %1$s for the entire day',
+ 'In %n weeks on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 weeks on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a month on %1$s for the entire day',
+ 'In %n months on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 months on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a year on %1$s for the entire day',
+ 'In %n years on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 years on July 1, 2024 for the entire day'
+ ],
+ // singular partial day
+ [
+ 'In a minute on %1$s between %2$s - %3$s',
+ 'In %n minutes on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a minute on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a hour on %1$s between %2$s - %3$s',
+ 'In %n hours on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a hour on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a day on %1$s between %2$s - %3$s',
+ 'In %n days on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a week on %1$s between %2$s - %3$s',
+ 'In %n weeks on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a week on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a month on %1$s between %2$s - %3$s',
+ 'In %n months on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a month on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a year on %1$s between %2$s - %3$s',
+ 'In %n years on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ // plural partial day
+ [
+ 'In a minute on %1$s between %2$s - %3$s',
+ 'In %n minutes on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 minutes on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a hour on %1$s between %2$s - %3$s',
+ 'In %n hours on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 hours on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a day on %1$s between %2$s - %3$s',
+ 'In %n days on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 days on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a week on %1$s between %2$s - %3$s',
+ 'In %n weeks on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 weeks on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a month on %1$s between %2$s - %3$s',
+ 'In %n months on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 months on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a year on %1$s between %2$s - %3$s',
+ 'In %n years on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ ]);
+
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+ // past interval test dates
+ (new \DateTime('20240702T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240703T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240702T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240703T170000', (new \DateTimeZone('America/Toronto')))),
+ // minute interval test dates
+ (new \DateTime('20240701T075900', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240630T235900', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240701T075800', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240630T235800', (new \DateTimeZone('America/Toronto')))),
+ // hour interval test dates
+ (new \DateTime('20240701T070000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240630T230000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240701T060000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240630T220000', (new \DateTimeZone('America/Toronto')))),
+ // day interval test dates
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ // week interval test dates
+ (new \DateTime('20240621T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240621T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240614T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240614T170000', (new \DateTimeZone('America/Toronto')))),
+ // month interval test dates
+ (new \DateTime('20240530T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240530T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240430T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240430T170000', (new \DateTimeZone('America/Toronto')))),
+ // year interval test dates
+ (new \DateTime('20230630T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20230630T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20220630T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20220630T170000', (new \DateTimeZone('America/Toronto'))))
+ );
+
+ /** test partial day event in 1 day in the past*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In the past on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 day in the past*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In the past on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event in 2 days in the past*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In the past on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 days in the past*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In the past on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event in 1 minute*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In a minute on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 minute*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In a minute on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event in 2 minutes*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In 2 minutes on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 minutes*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In 2 minutes on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event in 1 hour*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In a hour on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 hour*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In a hour on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event in 2 hours*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In 2 hours on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 hours*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In 2 hours on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 day*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 day*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 days*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 days*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 week*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a week on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 week*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a week on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 weeks*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 weeks on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 weeks*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 weeks on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 month*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a month on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 month*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a month on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 months*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 months on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 months*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 months on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 year*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 year*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a year on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 years*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 years*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 years on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringDaily(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240713T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 13, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Day for the entire day', [], 'Every Day for the entire day'],
+ ['Every Day for the entire day until %1$s', ['July 13, 2024'], 'Every Day for the entire day until July 13, 2024'],
+ ['Every Day between %1$s - %2$s', ['8:00 AM', '9:00 AM (America/Toronto)'], 'Every Day between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Day between %1$s - %2$s until %3$s', ['8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Every %1$d Days for the entire day', [3], 'Every 3 Days for the entire day'],
+ ['Every %1$d Days for the entire day until %2$s', [3, 'July 13, 2024'], 'Every 3 Days for the entire day until July 13, 2024'],
+ ['Every %1$d Days between %2$s - %3$s', [3, '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Days between %2$s - %3$s until %4$s', [3, '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ]);
+
+ /** test partial day event with every day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event every 3rd day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every 3rd day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 3rd day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 3rd day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringWeekly(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240722T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 13, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Week on %1$s for the entire day', ['Monday, Wednesday, Friday'], 'Every Week on Monday, Wednesday, Friday for the entire day'],
+ ['Every Week on %1$s for the entire day until %2$s', ['Monday, Wednesday, Friday', 'July 13, 2024'], 'Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024'],
+ ['Every Week on %1$s between %2$s - %3$s', ['Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Week on %1$s between %2$s - %3$s until %4$s', ['Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Every %1$d Weeks on %2$s for the entire day', [2, 'Monday, Wednesday, Friday'], 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day'],
+ ['Every %1$d Weeks on %2$s for the entire day until %3$s', [2, 'Monday, Wednesday, Friday', 'July 13, 2024'], 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024'],
+ ['Every %1$d Weeks on %2$s between %3$s - %4$s', [2, 'Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s', [2, 'Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ['Monday', [], 'Monday'],
+ ['Wednesday', [], 'Wednesday'],
+ ['Friday', [], 'Friday'],
+ ]);
+
+ /** test partial day event with every week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every 2nd week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every 2nd week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 2nd week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 2nd week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringMonthly(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20241231T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'December 31, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Month on the %1$s for the entire day', ['1, 8'], 'Every Month on the 1, 8 for the entire day'],
+ ['Every Month on the %1$s for the entire day until %2$s', ['1, 8', 'December 31, 2024'], 'Every Month on the 1, 8 for the entire day until December 31, 2024'],
+ ['Every Month on the %1$s between %2$s - %3$s', ['1, 8', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Month on the %1$s between %2$s - %3$s until %4$s', ['1, 8', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Every %1$d Months on the %2$s for the entire day', [2, '1, 8'], 'Every 2 Months on the 1, 8 for the entire day'],
+ ['Every %1$d Months on the %2$s for the entire day until %3$s', [2, '1, 8', 'December 31, 2024'], 'Every 2 Months on the 1, 8 for the entire day until December 31, 2024'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s', [2, '1, 8', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [2, '1, 8', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Every Month on the %1$s for the entire day', ['First Sunday, Saturday'], 'Every Month on the First Sunday, Saturday for the entire day'],
+ ['Every Month on the %1$s for the entire day until %2$s', ['First Sunday, Saturday', 'December 31, 2024'], 'Every Month on the First Sunday, Saturday for the entire day until December 31, 2024'],
+ ['Every Month on the %1$s between %2$s - %3$s', ['First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Month on the %1$s between %2$s - %3$s until %4$s', ['First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Every %1$d Months on the %2$s for the entire day', [2, 'First Sunday, Saturday'], 'Every 2 Months on the First Sunday, Saturday for the entire day'],
+ ['Every %1$d Months on the %2$s for the entire day until %3$s', [2, 'First Sunday, Saturday', 'December 31, 2024'], 'Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s', [2, 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [2, 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ['Saturday', [], 'Saturday'],
+ ['Sunday', [], 'Sunday'],
+ ['First', [], 'First'],
+ ]);
+
+ /** test absolute partial day event with every month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd Month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every Month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd Month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd Month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every Month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd Month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringYearly(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20260731T040000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 31, 2026'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Year in %1$s on the %2$s for the entire day', ['July', '1st'], 'Every Year in July on the 1st for the entire day'],
+ ['Every Year in %1$s on the %2$s for the entire day until %3$s', ['July', '1st', 'July 31, 2026'], 'Every Year in July on the 1st for the entire day until July 31, 2026'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s', ['July', '1st', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', ['July', '1st', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day', [2, 'July', '1st'], 'Every 2 Years in July on the 1st for the entire day'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [2, 'July', '1st', 'July 31, 2026'], 'Every 2 Years in July on the 1st for the entire day until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [2, 'July', '1st', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [2, 'July', '1st', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Every Year in %1$s on the %2$s for the entire day', ['July', 'First Sunday, Saturday'], 'Every Year in July on the First Sunday, Saturday for the entire day'],
+ ['Every Year in %1$s on the %2$s for the entire day until %3$s', ['July', 'First Sunday, Saturday', 'July 31, 2026'], 'Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s', ['July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', ['July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day', [2, 'July', 'First Sunday, Saturday'], 'Every 2 Years in July on the First Sunday, Saturday for the entire day'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [2, 'July', 'First Sunday, Saturday', 'July 31, 2026'], 'Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [2, 'July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [2, 'July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ['July', [], 'July'],
+ ['Saturday', [], 'Saturday'],
+ ['Sunday', [], 'Sunday'],
+ ['First', [], 'First'],
+ ]);
+
+ /** test absolute partial day event with every year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;UNTIL=20260731T040000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringFixed(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240713T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 13, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['On specific dates for the entire day until %1$s', ['July 13, 2024'], 'On specific dates for the entire day until July 13, 2024'],
+ ['On specific dates between %1$s - %2$s until %3$s', ['8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ]);
+
+ /** test partial day event with every day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240709T080000,20240713T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240709T080000,20240713T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'On specific dates for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateOccurringStringWithRrule(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240703T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 3, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ // singular
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 1,
+ ['July 1, 2024'],
+ 'In a day on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 1,
+ ['July 1, 2024', 'July 3, 2024'],
+ 'In a day on July 1, 2024 then on July 3, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s and %3$s',
+ 'In %n days on %1$s then on %2$s and %3$s',
+ 1,
+ ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'],
+ 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024'
+ ],
+ // plural
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 days on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 2,
+ ['July 1, 2024', 'July 3, 2024'],
+ 'In 2 days on July 1, 2024 then on July 3, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s and %3$s',
+ 'In %n days on %1$s then on %2$s and %3$s',
+ 2,
+ ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'],
+ 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024'
+ ],
+ ]);
+
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ );
+
+ /** test patrial day recurring event in 1 day with single occurrence remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+
+ /** test patrial day recurring event in 1 day with two occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 3, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+
+ /** test patrial day recurring event in 1 day with three occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+
+ /** test patrial day recurring event in 2 days with single occurrence remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+
+ /** test patrial day recurring event in 2 days with two occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 3, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+
+ /** test patrial day recurring event in 2 days with three occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+ }
+
+ public function testGenerateOccurringStringWithRdate(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240703T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 3, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ // singular
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 1,
+ ['July 1, 2024'],
+ 'In a day on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 1,
+ ['July 1, 2024', 'July 3, 2024'],
+ 'In a day on July 1, 2024 then on July 3, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s and %3$s',
+ 'In %n days on %1$s then on %2$s and %3$s',
+ 1,
+ ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'],
+ 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024'
+ ],
+ // plural
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 days on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 2,
+ ['July 1, 2024', 'July 3, 2024'],
+ 'In 2 days on July 1, 2024 then on July 3, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s and %3$s',
+ 'In %n days on %1$s then on %2$s and %3$s',
+ 2,
+ ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'],
+ 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024'
+ ],
+ ]);
+
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ );
+
+ /** test patrial day recurring event in 1 day with single occurrence remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with single occurrence remaining'
+ );
+
+ /** test patrial day recurring event in 1 day with two occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000,20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 3, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with two occurrences remaining'
+ );
+
+ /** test patrial day recurring event in 1 day with three occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000,20240703T080000,20240705T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ ''
+ );
+
+ /** test patrial day recurring event in 2 days with single occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ ''
+ );
+
+ /** test patrial day recurring event in 2 days with two occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 3, 2024',
+ $this->service->generateOccurringString($eventReader),
+ ''
+ );
+
+ /** test patrial day recurring event in 2 days with three occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240705T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with three occurrences remaining'
+ );
+ }
+
+ public function testGenerateOccurringStringWithOneExdate(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240707T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 7, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ // singular
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 1,
+ ['July 1, 2024'],
+ 'In a day on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 1,
+ ['July 1, 2024', 'July 5, 2024'],
+ 'In a day on July 1, 2024 then on July 5, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s and %3$s',
+ 'In %n days on %1$s then on %2$s and %3$s',
+ 1,
+ ['July 1, 2024', 'July 5, 2024', 'July 7, 2024'],
+ 'In a day on July 1, 2024 then on July 5, 2024 and July 7, 2024'
+ ],
+ // plural
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 days on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 2,
+ ['July 1, 2024', 'July 5, 2024'],
+ 'In 2 days on July 1, 2024 then on July 5, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s and %3$s',
+ 'In %n days on %1$s then on %2$s and %3$s',
+ 2,
+ ['July 1, 2024', 'July 5, 2024', 'July 7, 2024'],
+ 'In 2 days on July 1, 2024 then on July 5, 2024 and July 7, 2024'
+ ],
+ ]);
+
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ );
+
+ /** test patrial day recurring event in 1 day with single occurrence remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with single occurrence remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 1 day with two occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with two occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 1 day with three occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with three occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 1 day with four occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=4');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 5, 2024 and July 7, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with four occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 2 days with single occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with single occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 2 days with two occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with two occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 2 days with three occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with three occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 2 days with four occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=4');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 5, 2024 and July 7, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with four occurrences remaining and one exception'
+ );
+ }
+
+ public function testGenerateOccurringStringWithTwoExdate(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240709T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 9, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ // singular
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 1,
+ ['July 1, 2024'],
+ 'In a day on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 1,
+ ['July 1, 2024', 'July 5, 2024'],
+ 'In a day on July 1, 2024 then on July 5, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s and %3$s',
+ 'In %n days on %1$s then on %2$s and %3$s',
+ 1,
+ ['July 1, 2024', 'July 5, 2024', 'July 9, 2024'],
+ 'In a day on July 1, 2024 then on July 5, 2024 and July 9, 2024'
+ ],
+ // plural
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 days on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 2,
+ ['July 1, 2024', 'July 5, 2024'],
+ 'In 2 days on July 1, 2024 then on July 5, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s and %3$s',
+ 'In %n days on %1$s then on %2$s and %3$s',
+ 2,
+ ['July 1, 2024', 'July 5, 2024', 'July 9, 2024'],
+ 'In 2 days on July 1, 2024 then on July 5, 2024 and July 9, 2024'
+ ],
+ ]);
+
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ );
+
+ /** test patrial day recurring event in 1 day with single occurrence remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with single occurrence remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 1 day with two occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with two occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 1 day with three occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with three occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 1 day with four occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=5');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 5, 2024 and July 9, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with four occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 2 days with single occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with single occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 2 days with two occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with two occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 2 days with three occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with three occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 2 days with five occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=5');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 5, 2024 and July 9, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with five occurrences remaining and two exception'
+ );
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php
new file mode 100644
index 00000000000..524ac556e19
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php
@@ -0,0 +1,770 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Schedule;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\CalDAV\Plugin as CalDAVPlugin;
+use OCA\DAV\CalDAV\Schedule\Plugin;
+use OCA\DAV\CalDAV\Trashbin\Plugin as TrashbinPlugin;
+use OCP\IConfig;
+use OCP\IL10N;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\CalDAV\Backend\BackendInterface;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\DAV\Xml\Property\LocalHref;
+use Sabre\DAVACL\IPrincipal;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\ITip\Message;
+use Sabre\VObject\Parameter;
+use Sabre\VObject\Property\ICalendar\CalAddress;
+use Sabre\Xml\Service;
+use Test\TestCase;
+
+class PluginTest extends TestCase {
+ private Plugin $plugin;
+ private Server&MockObject $server;
+ private IConfig&MockObject $config;
+ private LoggerInterface&MockObject $logger;
+ private DefaultCalendarValidator $calendarValidator;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->calendarValidator = new DefaultCalendarValidator();
+
+ $this->server = $this->createMock(Server::class);
+ $this->server->httpResponse = $this->createMock(ResponseInterface::class);
+ $this->server->xml = new Service();
+
+ $this->plugin = new Plugin($this->config, $this->logger, $this->calendarValidator);
+ $this->plugin->initialize($this->server);
+ }
+
+ public function testInitialize(): void {
+ $calls = [
+ // Sabre\CalDAV\Schedule\Plugin events
+ ['method:POST', [$this->plugin, 'httpPost'], 100],
+ ['propFind', [$this->plugin, 'propFind'], 100],
+ ['propPatch', [$this->plugin, 'propPatch'], 100],
+ ['calendarObjectChange', [$this->plugin, 'calendarObjectChange'], 100],
+ ['beforeUnbind', [$this->plugin, 'beforeUnbind'], 100],
+ ['schedule', [$this->plugin, 'scheduleLocalDelivery'], 100],
+ ['getSupportedPrivilegeSet', [$this->plugin, 'getSupportedPrivilegeSet'], 100],
+ // OCA\DAV\CalDAV\Schedule\Plugin events
+ ['propFind', [$this->plugin, 'propFindDefaultCalendarUrl'], 90],
+ ['afterWriteContent', [$this->plugin, 'dispatchSchedulingResponses'], 100],
+ ['afterCreateFile', [$this->plugin, 'dispatchSchedulingResponses'], 100],
+ ];
+ $this->server->expects($this->exactly(count($calls)))
+ ->method('on')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ $this->plugin->initialize($this->server);
+ }
+
+ public function testGetAddressesForPrincipal(): void {
+ $href = $this->createMock(Href::class);
+ $href
+ ->expects($this->once())
+ ->method('getHrefs')
+ ->willReturn(['lukas@nextcloud.com', 'rullzer@nextcloud.com']);
+ $this->server
+ ->expects($this->once())
+ ->method('getProperties')
+ ->with(
+ 'MyPrincipal',
+ [
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set',
+ ]
+ )
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => $href
+ ]);
+
+ $result = $this->invokePrivate($this->plugin, 'getAddressesForPrincipal', ['MyPrincipal']);
+ $this->assertSame(['lukas@nextcloud.com', 'rullzer@nextcloud.com'], $result);
+ }
+
+ public function testGetAddressesForPrincipalEmpty(): void {
+ $this->server
+ ->expects($this->once())
+ ->method('getProperties')
+ ->with(
+ 'MyPrincipal',
+ [
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set',
+ ]
+ )
+ ->willReturn(null);
+
+ $result = $this->invokePrivate($this->plugin, 'getAddressesForPrincipal', ['MyPrincipal']);
+ $this->assertSame([], $result);
+ }
+
+ public function testStripOffMailTo(): void {
+ $this->assertEquals('test@example.com', $this->invokePrivate($this->plugin, 'stripOffMailTo', ['test@example.com']));
+ $this->assertEquals('test@example.com', $this->invokePrivate($this->plugin, 'stripOffMailTo', ['mailto:test@example.com']));
+ }
+
+ public function testGetAttendeeRSVP(): void {
+ $property1 = $this->createMock(CalAddress::class);
+ $parameter1 = $this->createMock(Parameter::class);
+ $property1->expects($this->once())
+ ->method('offsetGet')
+ ->with('RSVP')
+ ->willReturn($parameter1);
+ $parameter1->expects($this->once())
+ ->method('getValue')
+ ->with()
+ ->willReturn('TRUE');
+
+ $property2 = $this->createMock(CalAddress::class);
+ $parameter2 = $this->createMock(Parameter::class);
+ $property2->expects($this->once())
+ ->method('offsetGet')
+ ->with('RSVP')
+ ->willReturn($parameter2);
+ $parameter2->expects($this->once())
+ ->method('getValue')
+ ->with()
+ ->willReturn('FALSE');
+
+ $property3 = $this->createMock(CalAddress::class);
+ $property3->expects($this->once())
+ ->method('offsetGet')
+ ->with('RSVP')
+ ->willReturn(null);
+
+ $this->assertTrue($this->invokePrivate($this->plugin, 'getAttendeeRSVP', [$property1]));
+ $this->assertFalse($this->invokePrivate($this->plugin, 'getAttendeeRSVP', [$property2]));
+ $this->assertFalse($this->invokePrivate($this->plugin, 'getAttendeeRSVP', [$property3]));
+ }
+
+ public static function propFindDefaultCalendarUrlProvider(): array {
+ return [
+ [
+ 'principals/users/myuser',
+ 'calendars/myuser',
+ false,
+ CalDavBackend::PERSONAL_CALENDAR_URI,
+ CalDavBackend::PERSONAL_CALENDAR_NAME,
+ true
+ ],
+ [
+ 'principals/users/myuser',
+ 'calendars/myuser',
+ false,
+ CalDavBackend::PERSONAL_CALENDAR_URI,
+ CalDavBackend::PERSONAL_CALENDAR_NAME,
+ true,
+ true
+ ],
+ [
+ 'principals/users/myuser',
+ 'calendars/myuser',
+ false,
+ CalDavBackend::PERSONAL_CALENDAR_URI,
+ CalDavBackend::PERSONAL_CALENDAR_NAME,
+ false,
+ false,
+ true
+ ],
+ [
+ 'principals/users/myuser',
+ 'calendars/myuser',
+ false,
+ CalDavBackend::PERSONAL_CALENDAR_URI,
+ CalDavBackend::PERSONAL_CALENDAR_NAME,
+ false
+ ],
+ [
+ 'principals/users/myuser',
+ null,
+ false,
+ CalDavBackend::PERSONAL_CALENDAR_URI,
+ CalDavBackend::PERSONAL_CALENDAR_NAME,
+ true
+ ],
+ [
+ 'principals/users/myuser',
+ 'calendars/myuser',
+ false,
+ CalDavBackend::PERSONAL_CALENDAR_URI,
+ CalDavBackend::PERSONAL_CALENDAR_NAME,
+ true,
+ false,
+ false,
+ false,
+ ],
+ [
+ 'principals/users/myuser',
+ 'calendars/myuser',
+ false,
+ 'my_other_calendar',
+ 'My Other Calendar',
+ true
+ ],
+ [
+ 'principals/calendar-resources',
+ 'system-calendars/calendar-resources/myuser',
+ true,
+ CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI,
+ CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME,
+ true
+ ],
+ [
+ 'principals/calendar-resources',
+ 'system-calendars/calendar-resources/myuser',
+ true,
+ CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI,
+ CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME,
+ false
+ ],
+ [
+ 'principals/something-else',
+ 'calendars/whatever',
+ false,
+ CalDavBackend::PERSONAL_CALENDAR_URI,
+ CalDavBackend::PERSONAL_CALENDAR_NAME,
+ true
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('propFindDefaultCalendarUrlProvider')]
+ public function testPropFindDefaultCalendarUrl(string $principalUri, ?string $calendarHome, bool $isResource, string $calendarUri, string $displayName, bool $exists, bool $deleted = false, bool $hasExistingCalendars = false, bool $propertiesForPath = true): void {
+ $propFind = new PropFind(
+ $principalUri,
+ [
+ Plugin::SCHEDULE_DEFAULT_CALENDAR_URL
+ ],
+ 0
+ );
+ /** @var IPrincipal&MockObject $node */
+ $node = $this->getMockBuilder(IPrincipal::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $node->expects($this->once())
+ ->method('getPrincipalUrl')
+ ->with()
+ ->willReturn($principalUri);
+
+ $calDAVPlugin = $this->getMockBuilder(CalDAVPlugin::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $calDAVPlugin->expects($this->once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn($calendarHome);
+
+ $this->server->expects($this->once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($calDAVPlugin);
+ if (!$calendarHome) {
+ $this->plugin->propFindDefaultCalendarUrl($propFind, $node);
+
+ $this->assertNull($propFind->get(Plugin::SCHEDULE_DEFAULT_CALENDAR_URL));
+ return;
+ }
+ if ($principalUri === 'principals/something-else') {
+ $this->plugin->propFindDefaultCalendarUrl($propFind, $node);
+
+ $this->assertNull($propFind->get(Plugin::SCHEDULE_DEFAULT_CALENDAR_URL));
+ return;
+ }
+
+ if (!$isResource) {
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('myuser', 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI)
+ ->willReturn($calendarUri);
+ }
+
+ $calendarHomeObject = $this->createMock(CalendarHome::class);
+ $calendarHomeObject->expects($this->once())
+ ->method('childExists')
+ ->with($calendarUri)
+ ->willReturn($exists);
+
+ if ($exists) {
+ $calendar = $this->createMock(Calendar::class);
+ $calendar->expects($this->once())->method('isDeleted')->willReturn($deleted);
+ $calendarHomeObject->expects($deleted && !$hasExistingCalendars ? $this->exactly(2) : $this->once())->method('getChild')->with($calendarUri)->willReturn($calendar);
+ }
+
+ $calendarBackend = $this->createMock(CalDavBackend::class);
+ $calendarUri = $hasExistingCalendars ? 'custom' : $calendarUri;
+ $displayName = $hasExistingCalendars ? 'Custom Calendar' : $displayName;
+
+ $existingCalendars = $hasExistingCalendars ? [
+ new Calendar(
+ $calendarBackend,
+ ['uri' => 'deleted', '{DAV:}displayname' => 'A deleted calendar', TrashbinPlugin::PROPERTY_DELETED_AT => 42],
+ $this->createMock(IL10N::class),
+ $this->config,
+ $this->createMock(LoggerInterface::class)
+ ),
+ new Calendar(
+ $calendarBackend,
+ ['uri' => $calendarUri, '{DAV:}displayname' => $displayName],
+ $this->createMock(IL10N::class),
+ $this->config,
+ $this->createMock(LoggerInterface::class)
+ )
+ ] : [];
+
+ if (!$exists || $deleted) {
+ if (!$hasExistingCalendars) {
+ $calendarBackend->expects($this->once())
+ ->method('createCalendar')
+ ->with($principalUri, $calendarUri, [
+ '{DAV:}displayname' => $displayName,
+ ]);
+
+ $calendarHomeObject->expects($this->exactly($deleted ? 2 : 1))
+ ->method('getCalDAVBackend')
+ ->with()
+ ->willReturn($calendarBackend);
+ }
+
+ if (!$isResource) {
+ $calendarHomeObject->expects($this->once())
+ ->method('getChildren')
+ ->with()
+ ->willReturn($existingCalendars);
+ }
+ }
+
+ /** @var Tree&MockObject $tree */
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with($calendarHome)
+ ->willReturn($calendarHomeObject);
+ $this->server->tree = $tree;
+
+ $properties = $propertiesForPath ? [
+ ['href' => '/remote.php/dav/' . $calendarHome . '/' . $calendarUri]
+ ] : [];
+
+ $this->server->expects($this->once())
+ ->method('getPropertiesForPath')
+ ->with($calendarHome . '/' . $calendarUri, [], 1)
+ ->willReturn($properties);
+
+ $this->plugin->propFindDefaultCalendarUrl($propFind, $node);
+
+ if (!$propertiesForPath) {
+ $this->assertNull($propFind->get(Plugin::SCHEDULE_DEFAULT_CALENDAR_URL));
+ return;
+ }
+
+ /** @var LocalHref $result */
+ $result = $propFind->get(Plugin::SCHEDULE_DEFAULT_CALENDAR_URL);
+ $this->assertEquals('/remote.php/dav/' . $calendarHome . '/' . $calendarUri, $result->getHref());
+ }
+
+ /**
+ * Test Calendar Event Creation for Personal Calendar
+ *
+ * Should generate 2 messages for attendees User 2 and User External
+ */
+ public function testCalendarObjectChangePersonalCalendarCreate(): void {
+
+ // define place holders
+ /** @var Message[] $iTipMessages */
+ $iTipMessages = [];
+ // construct calendar node
+ $calendarNode = new Calendar(
+ $this->createMock(BackendInterface::class),
+ [
+ 'uri' => 'personal',
+ 'principaluri' => 'principals/users/user1',
+ '{DAV:}displayname' => 'Calendar Shared By User1',
+ ],
+ $this->createMock(IL10N::class),
+ $this->config,
+ $this->logger
+ );
+ // construct server request object
+ $request = new Request(
+ 'PUT',
+ '/remote.php/dav/calendars/user1/personal/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics'
+ );
+ $request->setBaseUrl('/remote.php/dav/');
+ // construct server response object
+ $response = new Response();
+ // construct server tree object
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('calendars/user1/personal')
+ ->willReturn($calendarNode);
+ // construct server properties and returns
+ $this->server->httpRequest = $request;
+ $this->server->tree = $tree;
+ $this->server->expects($this->exactly(1))->method('getProperties')
+ ->willReturnMap([
+ [
+ 'principals/users/user1',
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'],
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref(
+ ['mailto:user1@testing.local','/remote.php/dav/principals/users/user1/']
+ )]
+ ]
+ ]);
+ $this->server->expects($this->exactly(2))->method('emit')->willReturnCallback(
+ function (string $eventName, array $arguments = [], ?callable $continueCallBack = null) use (&$iTipMessages) {
+ $this->assertEquals('schedule', $eventName);
+ $this->assertCount(1, $arguments);
+ $iTipMessages[] = $arguments[0];
+ return true;
+ }
+ );
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurring Event');
+ $vEvent->add('ORGANIZER', 'mailto:user1@testing.local', ['CN' => 'User One']);
+ $vEvent->add('ATTENDEE', 'mailto:user2@testing.local', [
+ 'CN' => 'User Two',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $vEvent->add('ATTENDEE', 'mailto:user@external.local', [
+ 'CN' => 'User External',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // define flags
+ $newFlag = true;
+ $modifiedFlag = false;
+ // execute method
+ $this->plugin->calendarObjectChange(
+ $request,
+ $response,
+ $vCalendar,
+ 'calendars/user1/personal',
+ $modifiedFlag,
+ $newFlag
+ );
+ // test for correct iTip message count
+ $this->assertCount(2, $iTipMessages);
+ // test for Sharer Attendee
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[0]->sender);
+ $this->assertEquals('mailto:user2@testing.local', $iTipMessages[0]->recipient);
+ $this->assertTrue($iTipMessages[0]->significantChange);
+ // test for External Attendee
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[1]->sender);
+ $this->assertEquals('mailto:user@external.local', $iTipMessages[1]->recipient);
+ $this->assertTrue($iTipMessages[1]->significantChange);
+
+ }
+
+ /**
+ * Test Calendar Event Creation for Shared Calendar as Sharer/Owner
+ *
+ * Should generate 3 messages for attendees User 2 (Sharee), User 3 (Non-Sharee) and User External
+ */
+ public function testCalendarObjectChangeSharedCalendarSharerCreate(): void {
+
+ // define place holders
+ /** @var Message[] $iTipMessages */
+ $iTipMessages = [];
+ // construct calendar node
+ $calendarNode = new Calendar(
+ $this->createMock(BackendInterface::class),
+ [
+ 'uri' => 'calendar_shared_by_user1',
+ 'principaluri' => 'principals/users/user1',
+ '{DAV:}displayname' => 'Calendar Shared By User1',
+ '{http://owncloud.org/ns}owner-principal' => 'principals/users/user1'
+ ],
+ $this->createMock(IL10N::class),
+ $this->config,
+ $this->logger
+ );
+ // construct server request object
+ $request = new Request(
+ 'PUT',
+ '/remote.php/dav/calendars/user1/calendar_shared_by_user1/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics'
+ );
+ $request->setBaseUrl('/remote.php/dav/');
+ // construct server response object
+ $response = new Response();
+ // construct server tree object
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('calendars/user1/calendar_shared_by_user1')
+ ->willReturn($calendarNode);
+ // construct server properties and returns
+ $this->server->httpRequest = $request;
+ $this->server->tree = $tree;
+ $this->server->expects($this->exactly(1))->method('getProperties')
+ ->willReturnMap([
+ [
+ 'principals/users/user1',
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'],
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref(
+ ['mailto:user1@testing.local','/remote.php/dav/principals/users/user1/']
+ )]
+ ]
+ ]);
+ $this->server->expects($this->exactly(3))->method('emit')->willReturnCallback(
+ function (string $eventName, array $arguments = [], ?callable $continueCallBack = null) use (&$iTipMessages) {
+ $this->assertEquals('schedule', $eventName);
+ $this->assertCount(1, $arguments);
+ $iTipMessages[] = $arguments[0];
+ return true;
+ }
+ );
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurring Event');
+ $vEvent->add('ORGANIZER', 'mailto:user1@testing.local', ['CN' => 'User One']);
+ $vEvent->add('ATTENDEE', 'mailto:user2@testing.local', [
+ 'CN' => 'User Two',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $vEvent->add('ATTENDEE', 'mailto:user3@testing.local', [
+ 'CN' => 'User Three',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $vEvent->add('ATTENDEE', 'mailto:user@external.local', [
+ 'CN' => 'User External',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // define flags
+ $newFlag = true;
+ $modifiedFlag = false;
+ // execute method
+ $this->plugin->calendarObjectChange(
+ $request,
+ $response,
+ $vCalendar,
+ 'calendars/user1/calendar_shared_by_user1',
+ $modifiedFlag,
+ $newFlag
+ );
+ // test for correct iTip message count
+ $this->assertCount(3, $iTipMessages);
+ // test for Sharer Attendee
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[0]->sender);
+ $this->assertEquals('mailto:user2@testing.local', $iTipMessages[0]->recipient);
+ $this->assertTrue($iTipMessages[0]->significantChange);
+ // test for Non Shee Attendee
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[1]->sender);
+ $this->assertEquals('mailto:user3@testing.local', $iTipMessages[1]->recipient);
+ $this->assertTrue($iTipMessages[1]->significantChange);
+ // test for External Attendee
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[2]->sender);
+ $this->assertEquals('mailto:user@external.local', $iTipMessages[2]->recipient);
+ $this->assertTrue($iTipMessages[2]->significantChange);
+
+ }
+
+ /**
+ * Test Calendar Event Creation for Shared Calendar as Shree
+ *
+ * Should generate 3 messages for attendees User 1 (Sharer/Owner), User 3 (Non-Sharee) and User External
+ */
+ public function testCalendarObjectChangeSharedCalendarShreeCreate(): void {
+
+ // define place holders
+ /** @var Message[] $iTipMessages */
+ $iTipMessages = [];
+ // construct calendar node
+ $calendarNode = new Calendar(
+ $this->createMock(BackendInterface::class),
+ [
+ 'uri' => 'calendar_shared_by_user1',
+ 'principaluri' => 'principals/users/user2',
+ '{DAV:}displayname' => 'Calendar Shared By User1',
+ '{http://owncloud.org/ns}owner-principal' => 'principals/users/user1'
+ ],
+ $this->createMock(IL10N::class),
+ $this->config,
+ $this->logger
+ );
+ // construct server request object
+ $request = new Request(
+ 'PUT',
+ '/remote.php/dav/calendars/user2/calendar_shared_by_user1/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics'
+ );
+ $request->setBaseUrl('/remote.php/dav/');
+ // construct server response object
+ $response = new Response();
+ // construct server tree object
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('calendars/user2/calendar_shared_by_user1')
+ ->willReturn($calendarNode);
+ // construct server properties and returns
+ $this->server->httpRequest = $request;
+ $this->server->tree = $tree;
+ $this->server->expects($this->exactly(2))->method('getProperties')
+ ->willReturnMap([
+ [
+ 'principals/users/user1',
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'],
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref(
+ ['mailto:user1@testing.local','/remote.php/dav/principals/users/user1/']
+ )]
+ ],
+ [
+ 'principals/users/user2',
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'],
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref(
+ ['mailto:user2@testing.local','/remote.php/dav/principals/users/user2/']
+ )]
+ ]
+ ]);
+ $this->server->expects($this->exactly(3))->method('emit')->willReturnCallback(
+ function (string $eventName, array $arguments = [], ?callable $continueCallBack = null) use (&$iTipMessages) {
+ $this->assertEquals('schedule', $eventName);
+ $this->assertCount(1, $arguments);
+ $iTipMessages[] = $arguments[0];
+ return true;
+ }
+ );
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurring Event');
+ $vEvent->add('ORGANIZER', 'mailto:user2@testing.local', ['CN' => 'User Two']);
+ $vEvent->add('ATTENDEE', 'mailto:user1@testing.local', [
+ 'CN' => 'User One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $vEvent->add('ATTENDEE', 'mailto:user3@testing.local', [
+ 'CN' => 'User Three',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $vEvent->add('ATTENDEE', 'mailto:user@external.local', [
+ 'CN' => 'User External',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // define flags
+ $newFlag = true;
+ $modifiedFlag = false;
+ // execute method
+ $this->plugin->calendarObjectChange(
+ $request,
+ $response,
+ $vCalendar,
+ 'calendars/user2/calendar_shared_by_user1',
+ $modifiedFlag,
+ $newFlag
+ );
+ // test for correct iTip message count
+ $this->assertCount(3, $iTipMessages);
+ // test for Sharer Attendee
+ $this->assertEquals('mailto:user2@testing.local', $iTipMessages[0]->sender);
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[0]->recipient);
+ $this->assertTrue($iTipMessages[0]->significantChange);
+ // test for Non Shee Attendee
+ $this->assertEquals('mailto:user2@testing.local', $iTipMessages[1]->sender);
+ $this->assertEquals('mailto:user3@testing.local', $iTipMessages[1]->recipient);
+ $this->assertTrue($iTipMessages[1]->significantChange);
+ // test for External Attendee
+ $this->assertEquals('mailto:user2@testing.local', $iTipMessages[2]->sender);
+ $this->assertEquals('mailto:user@external.local', $iTipMessages[2]->recipient);
+ $this->assertTrue($iTipMessages[2]->significantChange);
+
+ }
+
+ /**
+ * Test Calendar Event Creation with iTip and iMip disabled
+ *
+ * Should generate 2 messages for attendees User 2 and User External
+ */
+ public function testCalendarObjectChangeWithSchedulingDisabled(): void {
+ // construct server request
+ $request = new Request(
+ 'PUT',
+ '/remote.php/dav/calendars/user1/personal/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics',
+ ['x-nc-scheduling' => 'false']
+ );
+ $request->setBaseUrl('/remote.php/dav/');
+ // construct server response
+ $response = new Response();
+ // construct server tree
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->never())
+ ->method('getNodeForPath');
+ // construct server properties and returns
+ $this->server->httpRequest = $request;
+ $this->server->tree = $tree;
+ // construct empty calendar event
+ $vCalendar = new VCalendar();
+ $vEvent = $vCalendar->add('VEVENT', []);
+ // define flags
+ $newFlag = true;
+ $modifiedFlag = false;
+ // execute method
+ $this->plugin->calendarObjectChange(
+ $request,
+ $response,
+ $vCalendar,
+ 'calendars/user1/personal',
+ $modifiedFlag,
+ $newFlag
+ );
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php b/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php
new file mode 100644
index 00000000000..02ae504bce0
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php
@@ -0,0 +1,324 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Search\Xml\Request;
+
+use OCA\DAV\CalDAV\Search\Xml\Request\CalendarSearchReport;
+use Sabre\Xml\Reader;
+use Test\TestCase;
+
+class CalendarSearchReportTest extends TestCase {
+ private array $elementMap = [
+ '{http://nextcloud.com/ns}calendar-search'
+ => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport',
+ ];
+
+ public function testFoo(): void {
+ $xml = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
+ <d:prop>
+ <d:getetag />
+ <c:calendar-data />
+ </d:prop>
+ <nc:filter>
+ <nc:comp-filter name="VEVENT" />
+ <nc:comp-filter name="VTODO" />
+ <nc:prop-filter name="SUMMARY" />
+ <nc:prop-filter name="LOCATION" />
+ <nc:prop-filter name="ATTENDEE" />
+ <nc:param-filter property="ATTENDEE" name="CN" />
+ <nc:search-term>foo</nc:search-term>
+ </nc:filter>
+ <nc:limit>10</nc:limit>
+ <nc:offset>5</nc:offset>
+</nc:calendar-search>
+XML;
+
+ $result = $this->parse($xml);
+
+ $calendarSearchReport = new CalendarSearchReport();
+ $calendarSearchReport->properties = [
+ '{DAV:}getetag',
+ '{urn:ietf:params:xml:ns:caldav}calendar-data',
+ ];
+ $calendarSearchReport->filters = [
+ 'comps' => [
+ 'VEVENT',
+ 'VTODO'
+ ],
+ 'props' => [
+ 'SUMMARY',
+ 'LOCATION',
+ 'ATTENDEE'
+ ],
+ 'params' => [
+ [
+ 'property' => 'ATTENDEE',
+ 'parameter' => 'CN'
+ ]
+ ],
+ 'search-term' => 'foo'
+ ];
+ $calendarSearchReport->limit = 10;
+ $calendarSearchReport->offset = 5;
+
+ $this->assertEquals(
+ $calendarSearchReport,
+ $result['value']
+ );
+ }
+
+ public function testNoLimitOffset(): void {
+ $xml = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
+ <d:prop>
+ <d:getetag />
+ <c:calendar-data />
+ </d:prop>
+ <nc:filter>
+ <nc:comp-filter name="VEVENT" />
+ <nc:prop-filter name="SUMMARY" />
+ <nc:search-term>foo</nc:search-term>
+ </nc:filter>
+</nc:calendar-search>
+XML;
+
+ $result = $this->parse($xml);
+
+ $calendarSearchReport = new CalendarSearchReport();
+ $calendarSearchReport->properties = [
+ '{DAV:}getetag',
+ '{urn:ietf:params:xml:ns:caldav}calendar-data',
+ ];
+ $calendarSearchReport->filters = [
+ 'comps' => [
+ 'VEVENT',
+ ],
+ 'props' => [
+ 'SUMMARY',
+ ],
+ 'search-term' => 'foo'
+ ];
+ $calendarSearchReport->limit = null;
+ $calendarSearchReport->offset = null;
+
+ $this->assertEquals(
+ $calendarSearchReport,
+ $result['value']
+ );
+ }
+
+
+ public function testRequiresCompFilter(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+ $this->expectExceptionMessage('{http://nextcloud.com/ns}prop-filter or {http://nextcloud.com/ns}param-filter given without any {http://nextcloud.com/ns}comp-filter');
+
+ $xml = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
+ <d:prop>
+ <d:getetag />
+ <c:calendar-data />
+ </d:prop>
+ <nc:filter>
+ <nc:prop-filter name="SUMMARY" />
+ <nc:prop-filter name="LOCATION" />
+ <nc:prop-filter name="ATTENDEE" />
+ <nc:param-filter property="ATTENDEE" name="CN" />
+ <nc:search-term>foo</nc:search-term>
+ </nc:filter>
+ <nc:limit>10</nc:limit>
+ <nc:offset>5</nc:offset>
+</nc:calendar-search>
+XML;
+
+ $this->parse($xml);
+ }
+
+
+ public function testRequiresFilter(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+ $this->expectExceptionMessage('The {http://nextcloud.com/ns}filter element is required for this request');
+
+ $xml = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
+ <d:prop>
+ <d:getetag />
+ <c:calendar-data />
+ </d:prop>
+</nc:calendar-search>
+XML;
+
+ $this->parse($xml);
+ }
+
+
+ public function testNoSearchTerm(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+ $this->expectExceptionMessage('{http://nextcloud.com/ns}search-term is required for this request');
+
+ $xml = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
+ <d:prop>
+ <d:getetag />
+ <c:calendar-data />
+ </d:prop>
+ <nc:filter>
+ <nc:comp-filter name="VEVENT" />
+ <nc:comp-filter name="VTODO" />
+ <nc:prop-filter name="SUMMARY" />
+ <nc:prop-filter name="LOCATION" />
+ <nc:prop-filter name="ATTENDEE" />
+ <nc:param-filter property="ATTENDEE" name="CN" />
+ </nc:filter>
+ <nc:limit>10</nc:limit>
+ <nc:offset>5</nc:offset>
+</nc:calendar-search>
+XML;
+
+ $this->parse($xml);
+ }
+
+
+ public function testCompOnly(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+ $this->expectExceptionMessage('At least one{http://nextcloud.com/ns}prop-filter or {http://nextcloud.com/ns}param-filter is required for this request');
+
+ $xml = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
+ <d:prop>
+ <d:getetag />
+ <c:calendar-data />
+ </d:prop>
+ <nc:filter>
+ <nc:comp-filter name="VEVENT" />
+ <nc:comp-filter name="VTODO" />
+ <nc:search-term>foo</nc:search-term>
+ </nc:filter>
+</nc:calendar-search>
+XML;
+
+ $result = $this->parse($xml);
+
+ $calendarSearchReport = new CalendarSearchReport();
+ $calendarSearchReport->properties = [
+ '{DAV:}getetag',
+ '{urn:ietf:params:xml:ns:caldav}calendar-data',
+ ];
+ $calendarSearchReport->filters = [
+ 'comps' => [
+ 'VEVENT',
+ 'VTODO'
+ ],
+ 'search-term' => 'foo'
+ ];
+ $calendarSearchReport->limit = null;
+ $calendarSearchReport->offset = null;
+
+ $this->assertEquals(
+ $calendarSearchReport,
+ $result['value']
+ );
+ }
+
+ public function testPropOnly(): void {
+ $xml = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
+ <d:prop>
+ <d:getetag />
+ <c:calendar-data />
+ </d:prop>
+ <nc:filter>
+ <nc:comp-filter name="VEVENT" />
+ <nc:prop-filter name="SUMMARY" />
+ <nc:search-term>foo</nc:search-term>
+ </nc:filter>
+</nc:calendar-search>
+XML;
+
+ $result = $this->parse($xml);
+
+ $calendarSearchReport = new CalendarSearchReport();
+ $calendarSearchReport->properties = [
+ '{DAV:}getetag',
+ '{urn:ietf:params:xml:ns:caldav}calendar-data',
+ ];
+ $calendarSearchReport->filters = [
+ 'comps' => [
+ 'VEVENT',
+ ],
+ 'props' => [
+ 'SUMMARY',
+ ],
+ 'search-term' => 'foo'
+ ];
+ $calendarSearchReport->limit = null;
+ $calendarSearchReport->offset = null;
+
+ $this->assertEquals(
+ $calendarSearchReport,
+ $result['value']
+ );
+ }
+
+ public function testParamOnly(): void {
+ $xml = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
+ <d:prop>
+ <d:getetag />
+ <c:calendar-data />
+ </d:prop>
+ <nc:filter>
+ <nc:comp-filter name="VEVENT" />
+ <nc:param-filter property="ATTENDEE" name="CN" />
+ <nc:search-term>foo</nc:search-term>
+ </nc:filter>
+</nc:calendar-search>
+XML;
+
+ $result = $this->parse($xml);
+
+ $calendarSearchReport = new CalendarSearchReport();
+ $calendarSearchReport->properties = [
+ '{DAV:}getetag',
+ '{urn:ietf:params:xml:ns:caldav}calendar-data',
+ ];
+ $calendarSearchReport->filters = [
+ 'comps' => [
+ 'VEVENT',
+ ],
+ 'params' => [
+ [
+ 'property' => 'ATTENDEE',
+ 'parameter' => 'CN'
+ ]
+ ],
+ 'search-term' => 'foo'
+ ];
+ $calendarSearchReport->limit = null;
+ $calendarSearchReport->offset = null;
+
+ $this->assertEquals(
+ $calendarSearchReport,
+ $result['value']
+ );
+ }
+
+ private function parse(string $xml, array $elementMap = []): array {
+ $reader = new Reader();
+ $reader->elementMap = array_merge($this->elementMap, $elementMap);
+ $reader->xml($xml);
+ return $reader->parse();
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php b/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php
new file mode 100644
index 00000000000..e576fbae34c
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php
@@ -0,0 +1,117 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Search;
+
+use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\Search\SearchPlugin;
+use OCA\DAV\CalDAV\Search\Xml\Request\CalendarSearchReport;
+use Sabre\Xml\Service;
+use Test\TestCase;
+
+class SearchPluginTest extends TestCase {
+ protected $server;
+
+ /** @var SearchPlugin $plugin */
+ protected $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->server = $this->createMock(\Sabre\DAV\Server::class);
+ $this->server->tree = $this->createMock(\Sabre\DAV\Tree::class);
+ $this->server->httpResponse = $this->createMock(\Sabre\HTTP\Response::class);
+ $this->server->xml = new Service();
+
+ $this->plugin = new SearchPlugin();
+ $this->plugin->initialize($this->server);
+ }
+
+ public function testGetFeatures(): void {
+ $this->assertEquals(['nc-calendar-search'], $this->plugin->getFeatures());
+ }
+
+ public function testGetName(): void {
+ $this->assertEquals('nc-calendar-search', $this->plugin->getPluginName());
+ }
+
+ public function testInitialize(): void {
+ $server = $this->createMock(\Sabre\DAV\Server::class);
+
+ $plugin = new SearchPlugin();
+
+ $server->expects($this->once())
+ ->method('on')
+ ->with('report', [$plugin, 'report']);
+ $server->xml = new Service();
+
+ $plugin->initialize($server);
+
+ $this->assertEquals(
+ $server->xml->elementMap['{http://nextcloud.com/ns}calendar-search'],
+ 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport'
+ );
+ }
+
+ public function testReportUnknown(): void {
+ $result = $this->plugin->report('{urn:ietf:params:xml:ns:caldav}calendar-query', 'REPORT', null);
+ $this->assertEquals($result, null);
+ $this->assertNotEquals($this->server->transactionType, 'report-nc-calendar-search');
+ }
+
+ public function testReport(): void {
+ $report = $this->createMock(CalendarSearchReport::class);
+ $report->filters = [];
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $this->server->expects($this->once())
+ ->method('getRequestUri')
+ ->with()
+ ->willReturn('/re/quest/u/r/i');
+ $this->server->tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('/re/quest/u/r/i')
+ ->willReturn($calendarHome);
+ $this->server->expects($this->once())
+ ->method('getHTTPDepth')
+ ->with(2)
+ ->willReturn(2);
+ $this->server
+ ->method('getHTTPPrefer')
+ ->willReturn([
+ 'return' => null
+ ]);
+ $calendarHome->expects($this->once())
+ ->method('calendarSearch')
+ ->willReturn([]);
+
+ $this->plugin->report('{http://nextcloud.com/ns}calendar-search', $report, '');
+ }
+
+ public function testSupportedReportSetNoCalendarHome(): void {
+ $this->server->tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('/foo/bar')
+ ->willReturn(null);
+
+ $reports = $this->plugin->getSupportedReportSet('/foo/bar');
+ $this->assertEquals([], $reports);
+ }
+
+ public function testSupportedReportSet(): void {
+ $calendarHome = $this->createMock(CalendarHome::class);
+
+ $this->server->tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('/bar/foo')
+ ->willReturn($calendarHome);
+
+ $reports = $this->plugin->getSupportedReportSet('/bar/foo');
+ $this->assertEquals([
+ '{http://nextcloud.com/ns}calendar-search'
+ ], $reports);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php b/apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php
new file mode 100644
index 00000000000..a5cf6a23c66
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php
@@ -0,0 +1,188 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV\Security;
+
+use OC\Security\RateLimiting\Exception\RateLimitExceededException;
+use OC\Security\RateLimiting\Limiter;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Security\RateLimitingPlugin;
+use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
+use OCP\IAppConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\Forbidden;
+use Test\TestCase;
+
+class RateLimitingPluginTest extends TestCase {
+
+ private Limiter&MockObject $limiter;
+ private CalDavBackend&MockObject $caldavBackend;
+ private IUserManager&MockObject $userManager;
+ private LoggerInterface&MockObject $logger;
+ private IAppConfig&MockObject $config;
+ private string $userId = 'user123';
+ private RateLimitingPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->limiter = $this->createMock(Limiter::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->caldavBackend = $this->createMock(CalDavBackend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->config = $this->createMock(IAppConfig::class);
+ $this->plugin = new RateLimitingPlugin(
+ $this->limiter,
+ $this->userManager,
+ $this->caldavBackend,
+ $this->logger,
+ $this->config,
+ $this->userId,
+ );
+ }
+
+ public function testNoUserObject(): void {
+ $this->limiter->expects(self::never())
+ ->method('registerUserRequest');
+
+ $this->plugin->beforeBind('calendars/foo/cal');
+ }
+
+ public function testUnrelated(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->limiter->expects(self::never())
+ ->method('registerUserRequest');
+
+ $this->plugin->beforeBind('foo/bar');
+ }
+
+ public function testRegisterCalendarCreation(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'caldav-create-calendar',
+ 10,
+ 3600,
+ $user,
+ );
+
+ $this->plugin->beforeBind('calendars/foo/cal');
+ }
+
+ public function testCalendarCreationRateLimitExceeded(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'caldav-create-calendar',
+ 10,
+ 3600,
+ $user,
+ )
+ ->willThrowException(new RateLimitExceededException());
+ $this->expectException(TooManyRequests::class);
+
+ $this->plugin->beforeBind('calendars/foo/cal');
+ }
+
+ public function testCalendarLimitReached(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $user->method('getUID')->willReturn('user123');
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'caldav-create-calendar',
+ 10,
+ 3600,
+ $user,
+ );
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarsForUserCount')
+ ->with('principals/users/user123')
+ ->willReturn(27);
+ $this->caldavBackend->expects(self::once())
+ ->method('getSubscriptionsForUserCount')
+ ->with('principals/users/user123')
+ ->willReturn(3);
+ $this->expectException(Forbidden::class);
+
+ $this->plugin->beforeBind('calendars/foo/cal');
+ }
+
+ public function testNoCalendarsSubscriptsLimit(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $user->method('getUID')->willReturn('user123');
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnCallback(function ($app, $key, $default) {
+ switch ($key) {
+ case 'maximumCalendarsSubscriptions':
+ return -1;
+ default:
+ return $default;
+ }
+ });
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'caldav-create-calendar',
+ 10,
+ 3600,
+ $user,
+ );
+ $this->caldavBackend->expects(self::never())
+ ->method('getCalendarsForUserCount')
+ ->with('principals/users/user123')
+ ->willReturn(27);
+ $this->caldavBackend->expects(self::never())
+ ->method('getSubscriptionsForUserCount')
+ ->with('principals/users/user123')
+ ->willReturn(3);
+
+ $this->plugin->beforeBind('calendars/foo/cal');
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php b/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php
new file mode 100644
index 00000000000..ee0ef2334ec
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php
@@ -0,0 +1,445 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Status;
+
+use OC\Calendar\CalendarQuery;
+use OCA\DAV\CalDAV\CalendarImpl;
+use OCA\DAV\CalDAV\Status\StatusService;
+use OCA\UserStatus\Db\UserStatus;
+use OCA\UserStatus\Service\StatusService as UserStatusService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Calendar\IManager;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\User\IAvailabilityCoordinator;
+use OCP\User\IOutOfOfficeData;
+use OCP\UserStatus\IUserStatus;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class StatusServiceTest extends TestCase {
+ private ITimeFactory&MockObject $timeFactory;
+ private IManager&MockObject $calendarManager;
+ private IUserManager&MockObject $userManager;
+ private UserStatusService&MockObject $userStatusService;
+ private IAvailabilityCoordinator&MockObject $availabilityCoordinator;
+ private ICacheFactory&MockObject $cacheFactory;
+ private LoggerInterface&MockObject $logger;
+ private ICache&MockObject $cache;
+ private StatusService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->calendarManager = $this->createMock(IManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userStatusService = $this->createMock(UserStatusService::class);
+ $this->availabilityCoordinator = $this->createMock(IAvailabilityCoordinator::class);
+ $this->cacheFactory = $this->createMock(ICacheFactory::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->cache = $this->createMock(ICache::class);
+ $this->cacheFactory->expects(self::once())
+ ->method('createLocal')
+ ->with('CalendarStatusService')
+ ->willReturn($this->cache);
+
+ $this->service = new StatusService($this->timeFactory,
+ $this->calendarManager,
+ $this->userManager,
+ $this->userStatusService,
+ $this->availabilityCoordinator,
+ $this->cacheFactory,
+ $this->logger,
+ );
+ }
+
+ public function testNoUser(): void {
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('getCurrentOutOfOfficeData');
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->logger->expects(self::never())
+ ->method('debug');
+ $this->cache->expects(self::never())
+ ->method('get');
+ $this->cache->expects(self::never())
+ ->method('set');
+ $this->calendarManager->expects(self::never())
+ ->method('getCalendarsForPrincipal');
+ $this->calendarManager->expects(self::never())
+ ->method('newQuery');
+ $this->timeFactory->expects(self::never())
+ ->method('getDateTime');
+ $this->calendarManager->expects(self::never())
+ ->method('searchForPrincipal');
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('findByUserId');
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testOOOInEffect(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn($this->createMock(IOutOfOfficeData::class));
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('isInEffect')
+ ->willReturn(true);
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->cache->expects(self::never())
+ ->method('get');
+ $this->cache->expects(self::never())
+ ->method('set');
+ $this->calendarManager->expects(self::never())
+ ->method('getCalendarsForPrincipal');
+ $this->calendarManager->expects(self::never())
+ ->method('newQuery');
+ $this->timeFactory->expects(self::never())
+ ->method('getDateTime');
+ $this->calendarManager->expects(self::never())
+ ->method('searchForPrincipal');
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('findByUserId');
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testNoCalendars(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([]);
+ $this->calendarManager->expects(self::never())
+ ->method('newQuery');
+ $this->timeFactory->expects(self::never())
+ ->method('getDateTime');
+ $this->calendarManager->expects(self::never())
+ ->method('searchForPrincipal');
+ $this->userStatusService->expects(self::once())
+ ->method('revertUserStatus');
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('findByUserId');
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testNoCalendarEvents(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([]);
+ $this->userStatusService->expects(self::once())
+ ->method('revertUserStatus');
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('findByUserId');
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testCalendarNoEventObjects(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->userStatusService->expects(self::once())
+ ->method('findByUserId')
+ ->willThrowException(new DoesNotExistException(''));
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([['objects' => []]]);
+ $this->userStatusService->expects(self::once())
+ ->method('revertUserStatus');
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testCalendarEvent(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->userStatusService->expects(self::once())
+ ->method('findByUserId')
+ ->willThrowException(new DoesNotExistException(''));
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([['objects' => [[]]]]);
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::once())
+ ->method('setUserStatus');
+
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testCallStatus(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([['objects' => [[]]]]);
+ $userStatus = new UserStatus();
+ $userStatus->setMessageId(IUserStatus::MESSAGE_CALL);
+ $userStatus->setStatusTimestamp(123456);
+ $this->userStatusService->expects(self::once())
+ ->method('findByUserId')
+ ->willReturn($userStatus);
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testInvisibleStatus(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([['objects' => [[]]]]);
+ $userStatus = new UserStatus();
+ $userStatus->setStatus(IUserStatus::INVISIBLE);
+ $userStatus->setStatusTimestamp(123456);
+ $this->userStatusService->expects(self::once())
+ ->method('findByUserId')
+ ->willReturn($userStatus);
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testDNDStatus(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([['objects' => [[]]]]);
+ $userStatus = new UserStatus();
+ $userStatus->setStatus(IUserStatus::DND);
+ $userStatus->setStatusTimestamp(123456);
+ $this->userStatusService->expects(self::once())
+ ->method('findByUserId')
+ ->willReturn($userStatus);
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+
+
+ $this->service->processCalendarStatus('admin');
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php b/apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php
new file mode 100644
index 00000000000..2d6d0e86358
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use DateTimeZone;
+use OCA\DAV\CalDAV\TimeZoneFactory;
+use Test\TestCase;
+
+class TimeZoneFactoryTest extends TestCase {
+
+ private TimeZoneFactory $factory;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->factory = new TimeZoneFactory();
+ }
+
+ public function testIsMS(): void {
+ // test Microsoft time zone
+ $this->assertTrue(TimeZoneFactory::isMS('Eastern Standard Time'));
+ // test IANA time zone
+ $this->assertFalse(TimeZoneFactory::isMS('America/Toronto'));
+ // test Fake time zone
+ $this->assertFalse(TimeZoneFactory::isMS('Fake Eastern Time'));
+ }
+
+ public function testToIana(): void {
+ // test Microsoft time zone
+ $this->assertEquals('America/Toronto', TimeZoneFactory::toIANA('Eastern Standard Time'));
+ // test IANA time zone
+ $this->assertEquals(null, TimeZoneFactory::toIANA('America/Toronto'));
+ // test Fake time zone
+ $this->assertEquals(null, TimeZoneFactory::toIANA('Fake Eastern Time'));
+ }
+
+ public function testFromName(): void {
+ // test Microsoft time zone
+ $this->assertEquals(new DateTimeZone('America/Toronto'), $this->factory->fromName('Eastern Standard Time'));
+ // test IANA time zone
+ $this->assertEquals(new DateTimeZone('America/Toronto'), $this->factory->fromName('America/Toronto'));
+ // test Fake time zone
+ $this->assertEquals(null, $this->factory->fromName('Fake Eastern Time'));
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php b/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php
new file mode 100644
index 00000000000..5bb87be67c1
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php
@@ -0,0 +1,142 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use DateTimeZone;
+use OCA\DAV\CalDAV\CalendarImpl;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Db\Property;
+use OCA\DAV\Db\PropertyMapper;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\IManager;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VTimeZone;
+use Test\TestCase;
+
+class TimezoneServiceTest extends TestCase {
+ private IConfig&MockObject $config;
+ private PropertyMapper&MockObject $propertyMapper;
+ private IManager&MockObject $calendarManager;
+ private TimezoneService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->propertyMapper = $this->createMock(PropertyMapper::class);
+ $this->calendarManager = $this->createMock(IManager::class);
+
+ $this->service = new TimezoneService(
+ $this->config,
+ $this->propertyMapper,
+ $this->calendarManager,
+ );
+ }
+
+ public function testGetUserTimezoneFromSettings(): void {
+ $this->config->expects(self::once())
+ ->method('getUserValue')
+ ->with('test123', 'core', 'timezone', '')
+ ->willReturn('Europe/Warsaw');
+
+ $timezone = $this->service->getUserTimezone('test123');
+
+ self::assertSame('Europe/Warsaw', $timezone);
+ }
+
+ public function testGetUserTimezoneFromAvailability(): void {
+ $this->config->expects(self::once())
+ ->method('getUserValue')
+ ->with('test123', 'core', 'timezone', '')
+ ->willReturn('');
+ $property = new Property();
+ $property->setPropertyvalue('BEGIN:VCALENDAR
+PRODID:Nextcloud DAV app
+BEGIN:VTIMEZONE
+TZID:Europe/Vienna
+END:VTIMEZONE
+END:VCALENDAR');
+ $this->propertyMapper->expects(self::once())
+ ->method('findPropertyByPathAndName')
+ ->willReturn([
+ $property,
+ ]);
+
+ $timezone = $this->service->getUserTimezone('test123');
+
+ self::assertNotNull($timezone);
+ self::assertEquals('Europe/Vienna', $timezone);
+ }
+
+ public function testGetUserTimezoneFromPersonalCalendar(): void {
+ $this->config->expects(self::exactly(2))
+ ->method('getUserValue')
+ ->willReturnMap([
+ ['test123', 'core', 'timezone', '', ''],
+ ['test123', 'dav', 'defaultCalendar', '', 'personal-1'],
+ ]);
+ $other = $this->createMock(ICalendar::class);
+ $other->method('getUri')->willReturn('other');
+ $personal = $this->createMock(CalendarImpl::class);
+ $personal->method('getUri')->willReturn('personal-1');
+ $tz = new DateTimeZone('Europe/Berlin');
+ $vtz = $this->createMock(VTimeZone::class);
+ $vtz->method('getTimeZone')->willReturn($tz);
+ $personal->method('getSchedulingTimezone')->willReturn($vtz);
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->with('principals/users/test123')
+ ->willReturn([
+ $other,
+ $personal,
+ ]);
+
+ $timezone = $this->service->getUserTimezone('test123');
+
+ self::assertNotNull($timezone);
+ self::assertEquals('Europe/Berlin', $timezone);
+ }
+
+ public function testGetUserTimezoneFromAny(): void {
+ $this->config->expects(self::exactly(2))
+ ->method('getUserValue')
+ ->willReturnMap([
+ ['test123', 'core', 'timezone', '', ''],
+ ['test123', 'dav', 'defaultCalendar', '', 'personal-1'],
+ ]);
+ $other = $this->createMock(ICalendar::class);
+ $other->method('getUri')->willReturn('other');
+ $personal = $this->createMock(CalendarImpl::class);
+ $personal->method('getUri')->willReturn('personal-2');
+ $tz = new DateTimeZone('Europe/Prague');
+ $vtz = $this->createMock(VTimeZone::class);
+ $vtz->method('getTimeZone')->willReturn($tz);
+ $personal->method('getSchedulingTimezone')->willReturn($vtz);
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->with('principals/users/test123')
+ ->willReturn([
+ $other,
+ $personal,
+ ]);
+
+ $timezone = $this->service->getUserTimezone('test123');
+
+ self::assertNotNull($timezone);
+ self::assertEquals('Europe/Prague', $timezone);
+ }
+
+ public function testGetUserTimezoneNoneFound(): void {
+ $timezone = $this->service->getUserTimezone('test123');
+
+ self::assertNull($timezone);
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/TipBrokerTest.php b/apps/dav/tests/unit/CalDAV/TipBrokerTest.php
new file mode 100644
index 00000000000..ddf992767d6
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/TipBrokerTest.php
@@ -0,0 +1,180 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\TipBroker;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+class TipBrokerTest extends TestCase {
+
+ private TipBroker $broker;
+ private VCalendar $vCalendar1a;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->broker = new TipBroker();
+ // construct calendar with a 1 hour event and same start/end time zones
+ $this->vCalendar1a = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $this->vCalendar1a->add('VEVENT', []);
+ $vEvent->add('UID', '96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTAMP', '20240701T000000Z');
+ $vEvent->add('CREATED', '20240701T000000Z');
+ $vEvent->add('LAST-MODIFIED', '20240701T000000Z');
+ $vEvent->add('SEQUENCE', '1');
+ $vEvent->add('STATUS', 'CONFIRMED');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ }
+
+ public function testParseEventForOrganizerOnCreate(): void {
+
+ // construct calendar and generate event info for newly created event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = [
+ 'organizer' => null,
+ 'significantChangeHash' => '',
+ 'attendees' => [],
+ ];
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnModify(): void {
+
+ // construct calendar and generate event info for modified event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->SUMMARY->setValue('Test Event Modified');
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnDelete(): void {
+
+ // construct calendar and generate event info for modified event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $currentEventInfo = $previousEventInfo;
+ $currentEventInfo['attendees'] = [];
+ ++$currentEventInfo['sequence'];
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('CANCEL', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnStatusCancelled(): void {
+
+ // construct calendar and generate event info for modified event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->STATUS->setValue('CANCELLED');
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('CANCEL', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnAddAttendee(): void {
+
+ // construct calendar and generate event info for modified event with two attendees
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
+ 'CN' => 'Attendee Two',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(2, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+ $this->assertEquals('REQUEST', $messages[1]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[1]->getValue(), $messages[1]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnRemoveAttendee(): void {
+
+ // construct calendar and generate event info for modified event with two attendees
+ $calendar = clone $this->vCalendar1a;
+ $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
+ 'CN' => 'Attendee Two',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->remove('ATTENDEE');
+ $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(2, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+ $this->assertEquals('CANCEL', $messages[1]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
+ $this->assertEquals('mailto:attendee2@testing.com', $messages[1]->recipient);
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php b/apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php
new file mode 100644
index 00000000000..74fb4b5e94e
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV\Validation;
+
+use OCA\DAV\CalDAV\Validation\CalDavValidatePlugin;
+use OCP\IAppConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+class CalDavValidatePluginTest extends TestCase {
+ private IAppConfig&MockObject $config;
+ private RequestInterface&MockObject $request;
+ private ResponseInterface&MockObject $response;
+
+ private CalDavValidatePlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+ // construct mock objects
+ $this->config = $this->createMock(IAppConfig::class);
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->response = $this->createMock(ResponseInterface::class);
+ $this->plugin = new CalDavValidatePlugin(
+ $this->config,
+ );
+ }
+
+ public function testPutSizeLessThenLimit(): void {
+
+ // construct method responses
+ $this->config
+ ->method('getValueInt')
+ ->with('dav', 'event_size_limit', 10485760)
+ ->willReturn(10485760);
+ $this->request
+ ->method('getRawServerValue')
+ ->with('CONTENT_LENGTH')
+ ->willReturn('1024');
+ // test condition
+ $this->assertTrue(
+ $this->plugin->beforePut($this->request, $this->response)
+ );
+
+ }
+
+ public function testPutSizeMoreThenLimit(): void {
+
+ // construct method responses
+ $this->config
+ ->method('getValueInt')
+ ->with('dav', 'event_size_limit', 10485760)
+ ->willReturn(10485760);
+ $this->request
+ ->method('getRawServerValue')
+ ->with('CONTENT_LENGTH')
+ ->willReturn('16242880');
+ $this->expectException(Forbidden::class);
+ // test condition
+ $this->plugin->beforePut($this->request, $this->response);
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php b/apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php
new file mode 100644
index 00000000000..c29415ecef3
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php
@@ -0,0 +1,176 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching;
+
+use OCA\DAV\CalDAV\WebcalCaching\Connection;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
+use OCP\Http\Client\IResponse;
+use OCP\Http\Client\LocalServerException;
+use OCP\IAppConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+use Test\TestCase;
+
+class ConnectionTest extends TestCase {
+
+ private IClientService&MockObject $clientService;
+ private IAppConfig&MockObject $config;
+ private LoggerInterface&MockObject $logger;
+ private Connection $connection;
+
+ public function setUp(): void {
+ $this->clientService = $this->createMock(IClientService::class);
+ $this->config = $this->createMock(IAppConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->connection = new Connection($this->clientService, $this->config, $this->logger);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('runLocalURLDataProvider')]
+ public function testLocalUrl($source): void {
+ $subscription = [
+ 'id' => 42,
+ 'uri' => 'sub123',
+ 'refreshreate' => 'P1H',
+ 'striptodos' => 1,
+ 'stripalarms' => 1,
+ 'stripattachments' => 1,
+ 'source' => $source,
+ 'lastmodified' => 0,
+ ];
+
+ $client = $this->createMock(IClient::class);
+ $this->clientService->expects(self::once())
+ ->method('newClient')
+ ->with()
+ ->willReturn($client);
+
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'webcalAllowLocalAccess', 'no')
+ ->willReturn('no');
+
+ $localServerException = new LocalServerException();
+ $client->expects(self::once())
+ ->method('get')
+ ->willThrowException($localServerException);
+ $this->logger->expects(self::once())
+ ->method('warning')
+ ->with('Subscription 42 was not refreshed because it violates local access rules', ['exception' => $localServerException]);
+
+ $this->connection->queryWebcalFeed($subscription);
+ }
+
+ public function testInvalidUrl(): void {
+ $subscription = [
+ 'id' => 42,
+ 'uri' => 'sub123',
+ 'refreshreate' => 'P1H',
+ 'striptodos' => 1,
+ 'stripalarms' => 1,
+ 'stripattachments' => 1,
+ 'source' => '!@#$',
+ 'lastmodified' => 0,
+ ];
+
+ $client = $this->createMock(IClient::class);
+ $this->config->expects(self::never())
+ ->method('getValueString');
+ $client->expects(self::never())
+ ->method('get');
+
+ $this->connection->queryWebcalFeed($subscription);
+
+ }
+
+ /**
+ * @param string $result
+ * @param string $contentType
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('urlDataProvider')]
+ public function testConnection(string $url, string $result, string $contentType): void {
+ $client = $this->createMock(IClient::class);
+ $response = $this->createMock(IResponse::class);
+ $subscription = [
+ 'id' => 42,
+ 'uri' => 'sub123',
+ 'refreshreate' => 'P1H',
+ 'striptodos' => 1,
+ 'stripalarms' => 1,
+ 'stripattachments' => 1,
+ 'source' => $url,
+ 'lastmodified' => 0,
+ ];
+
+ $this->clientService->expects($this->once())
+ ->method('newClient')
+ ->with()
+ ->willReturn($client);
+
+ $this->config->expects($this->once())
+ ->method('getValueString')
+ ->with('dav', 'webcalAllowLocalAccess', 'no')
+ ->willReturn('no');
+
+ $client->expects($this->once())
+ ->method('get')
+ ->with('https://foo.bar/bla2')
+ ->willReturn($response);
+
+ $response->expects($this->once())
+ ->method('getBody')
+ ->with()
+ ->willReturn($result);
+ $response->expects($this->once())
+ ->method('getHeader')
+ ->with('Content-Type')
+ ->willReturn($contentType);
+
+ $this->connection->queryWebcalFeed($subscription);
+ }
+
+ public static function runLocalURLDataProvider(): array {
+ return [
+ ['localhost/foo.bar'],
+ ['localHost/foo.bar'],
+ ['random-host/foo.bar'],
+ ['[::1]/bla.blub'],
+ ['[::]/bla.blub'],
+ ['192.168.0.1'],
+ ['172.16.42.1'],
+ ['[fdf8:f53b:82e4::53]/secret.ics'],
+ ['[fe80::200:5aee:feaa:20a2]/secret.ics'],
+ ['[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
+ ['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
+ ['10.0.0.1'],
+ ['another-host.local'],
+ ['service.localhost'],
+ ];
+ }
+
+ public static function urlDataProvider(): array {
+ return [
+ [
+ 'https://foo.bar/bla2',
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ 'text/calendar;charset=utf8',
+ ],
+ [
+ 'https://foo.bar/bla2',
+ '["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]',
+ 'application/calendar+json',
+ ],
+ [
+ 'https://foo.bar/bla2',
+ '<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>',
+ 'application/calendar+xml',
+ ],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php b/apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php
new file mode 100644
index 00000000000..804af021d5a
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php
@@ -0,0 +1,152 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching;
+
+use OCA\DAV\CalDAV\WebcalCaching\Plugin;
+use OCP\IRequest;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+
+class PluginTest extends \Test\TestCase {
+ public function testDisabled(): void {
+ $request = $this->createMock(IRequest::class);
+ $request->expects($this->once())
+ ->method('isUserAgent')
+ ->with(Plugin::ENABLE_FOR_CLIENTS)
+ ->willReturn(false);
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('X-NC-CalDAV-Webcal-Caching')
+ ->willReturn('');
+
+ $plugin = new Plugin($request);
+
+ $this->assertEquals(false, $plugin->isCachingEnabledForThisRequest());
+ }
+
+ public function testEnabledUserAgent(): void {
+ $request = $this->createMock(IRequest::class);
+ $request->expects($this->once())
+ ->method('isUserAgent')
+ ->with(Plugin::ENABLE_FOR_CLIENTS)
+ ->willReturn(true);
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('X-NC-CalDAV-Webcal-Caching')
+ ->willReturn('');
+ $request->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('REPORT');
+ $request->expects($this->never())
+ ->method('getParams');
+
+ $plugin = new Plugin($request);
+
+ $this->assertEquals(true, $plugin->isCachingEnabledForThisRequest());
+ }
+
+ public function testEnabledWebcalCachingHeader(): void {
+ $request = $this->createMock(IRequest::class);
+ $request->expects($this->once())
+ ->method('isUserAgent')
+ ->with(Plugin::ENABLE_FOR_CLIENTS)
+ ->willReturn(false);
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('X-NC-CalDAV-Webcal-Caching')
+ ->willReturn('On');
+ $request->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('REPORT');
+ $request->expects($this->never())
+ ->method('getParams');
+
+ $plugin = new Plugin($request);
+
+ $this->assertEquals(true, $plugin->isCachingEnabledForThisRequest());
+ }
+
+ public function testEnabledExportRequest(): void {
+ $request = $this->createMock(IRequest::class);
+ $request->expects($this->once())
+ ->method('isUserAgent')
+ ->with(Plugin::ENABLE_FOR_CLIENTS)
+ ->willReturn(false);
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('X-NC-CalDAV-Webcal-Caching')
+ ->willReturn('');
+ $request->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('GET');
+ $request->expects($this->once())
+ ->method('getParams')
+ ->willReturn(['export' => '']);
+
+ $plugin = new Plugin($request);
+
+ $this->assertEquals(true, $plugin->isCachingEnabledForThisRequest());
+ }
+
+ public function testSkipNonCalendarRequest(): void {
+ $request = $this->createMock(IRequest::class);
+ $request->expects($this->once())
+ ->method('isUserAgent')
+ ->with(Plugin::ENABLE_FOR_CLIENTS)
+ ->willReturn(false);
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('X-NC-CalDAV-Webcal-Caching')
+ ->willReturn('On');
+
+ $sabreRequest = new Request('REPORT', '/remote.php/dav/principals/users/admin/');
+ $sabreRequest->setBaseUrl('/remote.php/dav/');
+
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->never())
+ ->method('getNodeForPath');
+
+ $server = new Server($tree);
+
+ $plugin = new Plugin($request);
+ $plugin->initialize($server);
+
+ $plugin->beforeMethod($sabreRequest, new Response());
+ }
+
+ public function testProcessCalendarRequest(): void {
+ $request = $this->createMock(IRequest::class);
+ $request->expects($this->once())
+ ->method('isUserAgent')
+ ->with(Plugin::ENABLE_FOR_CLIENTS)
+ ->willReturn(false);
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('X-NC-CalDAV-Webcal-Caching')
+ ->willReturn('On');
+
+ $sabreRequest = new Request('REPORT', '/remote.php/dav/calendars/admin/personal/');
+ $sabreRequest->setBaseUrl('/remote.php/dav/');
+
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->once())
+ ->method('getNodeForPath');
+
+ $server = new Server($tree);
+
+ $plugin = new Plugin($request);
+ $plugin->initialize($server);
+
+ $plugin->beforeMethod($sabreRequest, new Response());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php b/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php
new file mode 100644
index 00000000000..d4f4b9e878f
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php
@@ -0,0 +1,325 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\WebcalCaching\Connection;
+use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\VObject;
+use Sabre\VObject\Recur\NoInstancesException;
+
+use Test\TestCase;
+
+class RefreshWebcalServiceTest extends TestCase {
+ private CalDavBackend&MockObject $caldavBackend;
+ private Connection&MockObject $connection;
+ private LoggerInterface&MockObject $logger;
+ private ITimeFactory&MockObject $time;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->caldavBackend = $this->createMock(CalDavBackend::class);
+ $this->connection = $this->createMock(Connection::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->time = $this->createMock(ITimeFactory::class);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
+ public function testRun(string $body, string $contentType, string $result): void {
+ $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
+ ->onlyMethods(['getRandomCalendarObjectUri'])
+ ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
+ ->getMock();
+
+ $refreshWebcalService
+ ->method('getRandomCalendarObjectUri')
+ ->willReturn('uri-1.ics');
+
+ $this->caldavBackend->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('principals/users/testuser')
+ ->willReturn([
+ [
+ 'id' => '99',
+ 'uri' => 'sub456',
+ RefreshWebcalService::REFRESH_RATE => 'P1D',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla',
+ 'lastmodified' => 0,
+ ],
+ [
+ 'id' => '42',
+ 'uri' => 'sub123',
+ RefreshWebcalService::REFRESH_RATE => 'PT1H',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla2',
+ 'lastmodified' => 0,
+ ],
+ ]);
+
+ $this->connection->expects(self::once())
+ ->method('queryWebcalFeed')
+ ->willReturn($result);
+ $this->caldavBackend->expects(self::once())
+ ->method('createCalendarObject')
+ ->with(42, 'uri-1.ics', $result, 1);
+
+ $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('identicalDataProvider')]
+ public function testRunIdentical(string $uid, array $calendarObject, string $body, string $contentType, string $result): void {
+ $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
+ ->onlyMethods(['getRandomCalendarObjectUri'])
+ ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
+ ->getMock();
+
+ $refreshWebcalService
+ ->method('getRandomCalendarObjectUri')
+ ->willReturn('uri-1.ics');
+
+ $this->caldavBackend->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('principals/users/testuser')
+ ->willReturn([
+ [
+ 'id' => '99',
+ 'uri' => 'sub456',
+ RefreshWebcalService::REFRESH_RATE => 'P1D',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla',
+ 'lastmodified' => 0,
+ ],
+ [
+ 'id' => '42',
+ 'uri' => 'sub123',
+ RefreshWebcalService::REFRESH_RATE => 'PT1H',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla2',
+ 'lastmodified' => 0,
+ ],
+ ]);
+
+ $this->connection->expects(self::once())
+ ->method('queryWebcalFeed')
+ ->willReturn($result);
+
+ $this->caldavBackend->expects(self::once())
+ ->method('getLimitedCalendarObjects')
+ ->willReturn($calendarObject);
+
+ $denormalised = [
+ 'etag' => 100,
+ 'size' => strlen($calendarObject[$uid]['calendardata']),
+ 'uid' => 'sub456'
+ ];
+
+ $this->caldavBackend->expects(self::once())
+ ->method('getDenormalizedData')
+ ->willReturn($denormalised);
+
+ $this->caldavBackend->expects(self::never())
+ ->method('createCalendarObject');
+
+ $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub456');
+ }
+
+ public function testRunJustUpdated(): void {
+ $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
+ ->onlyMethods(['getRandomCalendarObjectUri'])
+ ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
+ ->getMock();
+
+ $refreshWebcalService
+ ->method('getRandomCalendarObjectUri')
+ ->willReturn('uri-1.ics');
+
+ $this->caldavBackend->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('principals/users/testuser')
+ ->willReturn([
+ [
+ 'id' => '99',
+ 'uri' => 'sub456',
+ RefreshWebcalService::REFRESH_RATE => 'P1D',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla',
+ 'lastmodified' => time(),
+ ],
+ [
+ 'id' => '42',
+ 'uri' => 'sub123',
+ RefreshWebcalService::REFRESH_RATE => 'PT1H',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla2',
+ 'lastmodified' => time(),
+ ],
+ ]);
+
+ $timeMock = $this->createMock(\DateTime::class);
+ $this->time->expects(self::once())
+ ->method('getDateTime')
+ ->willReturn($timeMock);
+ $timeMock->expects(self::once())
+ ->method('getTimestamp')
+ ->willReturn(2101724667);
+ $this->time->expects(self::once())
+ ->method('getTime')
+ ->willReturn(time());
+ $this->connection->expects(self::never())
+ ->method('queryWebcalFeed');
+ $this->caldavBackend->expects(self::never())
+ ->method('createCalendarObject');
+
+ $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
+ public function testRunCreateCalendarNoException(string $body, string $contentType, string $result): void {
+ $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
+ ->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription',])
+ ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
+ ->getMock();
+
+ $refreshWebcalService
+ ->method('getRandomCalendarObjectUri')
+ ->willReturn('uri-1.ics');
+
+ $refreshWebcalService
+ ->method('getSubscription')
+ ->willReturn([
+ 'id' => '42',
+ 'uri' => 'sub123',
+ RefreshWebcalService::REFRESH_RATE => 'PT1H',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla2',
+ 'lastmodified' => 0,
+ ]);
+
+ $this->connection->expects(self::once())
+ ->method('queryWebcalFeed')
+ ->willReturn($result);
+
+ $this->caldavBackend->expects(self::once())
+ ->method('createCalendarObject')
+ ->with(42, 'uri-1.ics', $result, 1);
+
+ $noInstanceException = new NoInstancesException("can't add calendar object");
+ $this->caldavBackend->expects(self::once())
+ ->method('createCalendarObject')
+ ->willThrowException($noInstanceException);
+
+ $this->logger->expects(self::once())
+ ->method('warning')
+ ->with('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $noInstanceException, 'subscriptionId' => '42', 'source' => 'webcal://foo.bar/bla2']);
+
+ $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
+ public function testRunCreateCalendarBadRequest(string $body, string $contentType, string $result): void {
+ $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
+ ->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription'])
+ ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
+ ->getMock();
+
+ $refreshWebcalService
+ ->method('getRandomCalendarObjectUri')
+ ->willReturn('uri-1.ics');
+
+ $refreshWebcalService
+ ->method('getSubscription')
+ ->willReturn([
+ 'id' => '42',
+ 'uri' => 'sub123',
+ RefreshWebcalService::REFRESH_RATE => 'PT1H',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla2',
+ 'lastmodified' => 0,
+ ]);
+
+ $this->connection->expects(self::once())
+ ->method('queryWebcalFeed')
+ ->willReturn($result);
+
+ $this->caldavBackend->expects(self::once())
+ ->method('createCalendarObject')
+ ->with(42, 'uri-1.ics', $result, 1);
+
+ $badRequestException = new BadRequest("can't add reach calendar url");
+ $this->caldavBackend->expects(self::once())
+ ->method('createCalendarObject')
+ ->willThrowException($badRequestException);
+
+ $this->logger->expects(self::once())
+ ->method('warning')
+ ->with('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $badRequestException, 'subscriptionId' => '42', 'source' => 'webcal://foo.bar/bla2']);
+
+ $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
+ }
+
+ public static function identicalDataProvider(): array {
+ return [
+ [
+ '12345',
+ [
+ '12345' => [
+ 'id' => 42,
+ 'etag' => 100,
+ 'uri' => 'sub456',
+ 'calendardata' => "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ ],
+ ],
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ 'text/calendar;charset=utf8',
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20180218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ ],
+ ];
+ }
+
+ public static function runDataProvider(): array {
+ return [
+ [
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ 'text/calendar;charset=utf8',
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ ],
+ [
+ '["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]',
+ 'application/calendar+json',
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VTIMEZONE\r\nLAST-MODIFIED:20040110T032845Z\r\nTZID:US/Eastern\r\nBEGIN:DAYLIGHT\r\nDTSTART:20000404T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r\nTZNAME:EDT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nDTSTART:20001026T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=10\r\nTZNAME:EST\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060102T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
+ ],
+ [
+ '<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>',
+ 'application/calendar+xml',
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060104T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2 bis\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
+ ]
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/CapabilitiesTest.php b/apps/dav/tests/unit/CapabilitiesTest.php
new file mode 100644
index 00000000000..ad70d576d48
--- /dev/null
+++ b/apps/dav/tests/unit/CapabilitiesTest.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit;
+
+use OCA\DAV\Capabilities;
+use OCP\IConfig;
+use OCP\User\IAvailabilityCoordinator;
+use Test\TestCase;
+
+/**
+ * @package OCA\DAV\Tests\unit
+ */
+class CapabilitiesTest extends TestCase {
+ public function testGetCapabilities(): void {
+ $config = $this->createMock(IConfig::class);
+ $config->expects($this->once())
+ ->method('getSystemValueBool')
+ ->with('bulkupload.enabled', $this->isType('bool'))
+ ->willReturn(false);
+ $coordinator = $this->createMock(IAvailabilityCoordinator::class);
+ $coordinator->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(false);
+ $capabilities = new Capabilities($config, $coordinator);
+ $expected = [
+ 'dav' => [
+ 'chunking' => '1.0',
+ 'public_shares_chunking' => true,
+ ],
+ ];
+ $this->assertSame($expected, $capabilities->getCapabilities());
+ }
+
+ public function testGetCapabilitiesWithBulkUpload(): void {
+ $config = $this->createMock(IConfig::class);
+ $config->expects($this->once())
+ ->method('getSystemValueBool')
+ ->with('bulkupload.enabled', $this->isType('bool'))
+ ->willReturn(true);
+ $coordinator = $this->createMock(IAvailabilityCoordinator::class);
+ $coordinator->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(false);
+ $capabilities = new Capabilities($config, $coordinator);
+ $expected = [
+ 'dav' => [
+ 'chunking' => '1.0',
+ 'public_shares_chunking' => true,
+ 'bulkupload' => '1.0',
+ ],
+ ];
+ $this->assertSame($expected, $capabilities->getCapabilities());
+ }
+
+ public function testGetCapabilitiesWithAbsence(): void {
+ $config = $this->createMock(IConfig::class);
+ $config->expects($this->once())
+ ->method('getSystemValueBool')
+ ->with('bulkupload.enabled', $this->isType('bool'))
+ ->willReturn(false);
+ $coordinator = $this->createMock(IAvailabilityCoordinator::class);
+ $coordinator->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(true);
+ $capabilities = new Capabilities($config, $coordinator);
+ $expected = [
+ 'dav' => [
+ 'chunking' => '1.0',
+ 'public_shares_chunking' => true,
+ 'absence-supported' => true,
+ 'absence-replacement' => true,
+ ],
+ ];
+ $this->assertSame($expected, $capabilities->getCapabilities());
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/Activity/BackendTest.php b/apps/dav/tests/unit/CardDAV/Activity/BackendTest.php
new file mode 100644
index 00000000000..a070a3d7131
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/Activity/BackendTest.php
@@ -0,0 +1,483 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CardDAV\Activity;
+
+use OCA\DAV\CardDAV\Activity\Backend;
+use OCA\DAV\CardDAV\Activity\Provider\Addressbook;
+use OCA\DAV\CardDAV\Activity\Provider\Card;
+use OCP\Activity\IEvent;
+use OCP\Activity\IManager;
+use OCP\App\IAppManager;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class BackendTest extends TestCase {
+ protected IManager&MockObject $activityManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IUserSession&MockObject $userSession;
+ protected IAppManager&MockObject $appManager;
+ protected IUserManager&MockObject $userManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->activityManager = $this->createMock(IManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ }
+
+ /**
+ * @return Backend|MockObject
+ */
+ protected function getBackend(array $methods = []): Backend {
+ if (empty($methods)) {
+ return new Backend(
+ $this->activityManager,
+ $this->groupManager,
+ $this->userSession,
+ $this->appManager,
+ $this->userManager
+ );
+ } else {
+ return $this->getMockBuilder(Backend::class)
+ ->setConstructorArgs([
+ $this->activityManager,
+ $this->groupManager,
+ $this->userSession,
+ $this->appManager,
+ $this->userManager
+ ])
+ ->onlyMethods($methods)
+ ->getMock();
+ }
+ }
+
+ public static function dataCallTriggerAddressBookActivity(): array {
+ return [
+ ['onAddressbookCreate', [['data']], Addressbook::SUBJECT_ADD, [['data'], [], []]],
+ ['onAddressbookUpdate', [['data'], ['shares'], ['changed-properties']], Addressbook::SUBJECT_UPDATE, [['data'], ['shares'], ['changed-properties']]],
+ ['onAddressbookDelete', [['data'], ['shares']], Addressbook::SUBJECT_DELETE, [['data'], ['shares'], []]],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCallTriggerAddressBookActivity')]
+ public function testCallTriggerAddressBookActivity(string $method, array $payload, string $expectedSubject, array $expectedPayload): void {
+ $backend = $this->getBackend(['triggerAddressbookActivity']);
+ $backend->expects($this->once())
+ ->method('triggerAddressbookActivity')
+ ->willReturnCallback(function () use ($expectedPayload, $expectedSubject): void {
+ $arguments = func_get_args();
+ $this->assertSame($expectedSubject, array_shift($arguments));
+ $this->assertEquals($expectedPayload, $arguments);
+ });
+
+ call_user_func_array([$backend, $method], $payload);
+ }
+
+ public static function dataTriggerAddressBookActivity(): array {
+ return [
+ // Add addressbook
+ [Addressbook::SUBJECT_ADD, [], [], [], '', '', null, []],
+ [Addressbook::SUBJECT_ADD, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], [], [], '', 'admin', null, ['admin']],
+ [Addressbook::SUBJECT_ADD, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], [], [], 'test2', 'test2', null, ['admin']],
+
+ // Update addressbook
+ [Addressbook::SUBJECT_UPDATE, [], [], [], '', '', null, []],
+ // No visible change - owner only
+ [Addressbook::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], [], '', 'admin', null, ['admin']],
+ // Visible change
+ [Addressbook::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['{DAV:}displayname' => 'Name'], '', 'admin', ['user1'], ['user1', 'admin']],
+ [Addressbook::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['{DAV:}displayname' => 'Name'], 'test2', 'test2', ['user1'], ['user1', 'admin']],
+
+ // Delete addressbook
+ [Addressbook::SUBJECT_DELETE, [], [], [], '', '', null, []],
+ [Addressbook::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], [], '', 'admin', [], ['admin']],
+ [Addressbook::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], [], '', 'admin', ['user1'], ['user1', 'admin']],
+ [Addressbook::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], [], 'test2', 'test2', ['user1'], ['user1', 'admin']],
+ ];
+ }
+
+ /**
+ * @param string[]|null $shareUsers
+ * @param string[] $users
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTriggerAddressBookActivity')]
+ public function testTriggerAddressBookActivity(string $action, array $data, array $shares, array $changedProperties, string $currentUser, string $author, ?array $shareUsers, array $users): void {
+ $backend = $this->getBackend(['getUsersForShares']);
+
+ if ($shareUsers === null) {
+ $backend->expects($this->never())
+ ->method('getUsersForShares');
+ } else {
+ $backend->expects($this->once())
+ ->method('getUsersForShares')
+ ->with($shares)
+ ->willReturn($shareUsers);
+ }
+
+ if ($author !== '') {
+ if ($currentUser !== '') {
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->getUserMock($currentUser));
+ } else {
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn(null);
+ }
+
+ $event = $this->createMock(IEvent::class);
+ $this->activityManager->expects($this->once())
+ ->method('generateEvent')
+ ->willReturn($event);
+
+ $event->expects($this->once())
+ ->method('setApp')
+ ->with('dav')
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setObject')
+ ->with('addressbook', $data['id'])
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setType')
+ ->with('contacts')
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setAuthor')
+ ->with($author)
+ ->willReturnSelf();
+
+ $this->userManager->expects($action === Addressbook::SUBJECT_DELETE ? $this->exactly(sizeof($users)) : $this->never())
+ ->method('userExists')
+ ->willReturn(true);
+
+ $event->expects($this->exactly(count($users)))
+ ->method('setAffectedUser')
+ ->willReturnSelf();
+ $event->expects($this->exactly(count($users)))
+ ->method('setSubject')
+ ->willReturnSelf();
+ $this->activityManager->expects($this->exactly(count($users)))
+ ->method('publish')
+ ->with($event);
+ } else {
+ $this->activityManager->expects($this->never())
+ ->method('generateEvent');
+ }
+
+ $this->invokePrivate($backend, 'triggerAddressbookActivity', [$action, $data, $shares, $changedProperties]);
+ }
+
+ public function testNoAddressbookActivityCreatedForSystemAddressbook(): void {
+ $backend = $this->getBackend();
+ $this->activityManager->expects($this->never())
+ ->method('generateEvent');
+ $this->assertEmpty($this->invokePrivate($backend, 'triggerAddressbookActivity', [Addressbook::SUBJECT_ADD, ['principaluri' => 'principals/system/system'], [], [], '', '', null, []]));
+ }
+
+ public function testUserDeletionDoesNotCreateActivity(): void {
+ $backend = $this->getBackend();
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->willReturn(false);
+
+ $this->activityManager->expects($this->never())
+ ->method('publish');
+
+ $this->invokePrivate($backend, 'triggerAddressbookActivity', [Addressbook::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], [], []]);
+ }
+
+ public static function dataTriggerCardActivity(): array {
+ $cardData = "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.8//EN\r\nUID:test-user\r\nFN:test-user\r\nN:test-user;;;;\r\nEND:VCARD\r\n\r\n";
+
+ return [
+ // Add card
+ [Card::SUBJECT_ADD, [], [], [], '', '', null, []],
+ [Card::SUBJECT_ADD, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], [], [
+ 'carddata' => $cardData
+ ], '', 'admin', [], ['admin']],
+ [Card::SUBJECT_ADD, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], [], ['carddata' => $cardData], 'test2', 'test2', [], ['admin']],
+
+ // Update card
+ [Card::SUBJECT_UPDATE, [], [], [], '', '', null, []],
+ // No visible change - owner only
+ [Card::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], '', 'admin', [], ['admin']],
+ // Visible change
+ [Card::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], '', 'admin', ['user1'], ['user1', 'admin']],
+ [Card::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], 'test2', 'test2', ['user1'], ['user1', 'admin']],
+
+ // Delete card
+ [Card::SUBJECT_DELETE, [], [], ['carddata' => $cardData], '', '', null, []],
+ [Card::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], '', 'admin', [], ['admin']],
+ [Card::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], '', 'admin', ['user1'], ['user1', 'admin']],
+ [Card::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], 'test2', 'test2', ['user1'], ['user1', 'admin']],
+ ];
+ }
+
+ /**
+ * @param string[]|null $shareUsers
+ * @param string[] $users
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTriggerCardActivity')]
+ public function testTriggerCardActivity(string $action, array $addressBookData, array $shares, array $cardData, string $currentUser, string $author, ?array $shareUsers, array $users): void {
+ $backend = $this->getBackend(['getUsersForShares']);
+
+ if ($shareUsers === null) {
+ $backend->expects($this->never())
+ ->method('getUsersForShares');
+ } else {
+ $backend->expects($this->once())
+ ->method('getUsersForShares')
+ ->with($shares)
+ ->willReturn($shareUsers);
+ }
+
+ if ($author !== '') {
+ if ($currentUser !== '') {
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->getUserMock($currentUser));
+ } else {
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn(null);
+ }
+
+ $event = $this->createMock(IEvent::class);
+ $this->activityManager->expects($this->once())
+ ->method('generateEvent')
+ ->willReturn($event);
+
+ $event->expects($this->once())
+ ->method('setApp')
+ ->with('dav')
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setObject')
+ ->with('addressbook', $addressBookData['id'])
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setType')
+ ->with('contacts')
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setAuthor')
+ ->with($author)
+ ->willReturnSelf();
+
+ $event->expects($this->exactly(count($users)))
+ ->method('setAffectedUser')
+ ->willReturnSelf();
+ $event->expects($this->exactly(count($users)))
+ ->method('setSubject')
+ ->willReturnSelf();
+ $this->activityManager->expects($this->exactly(count($users)))
+ ->method('publish')
+ ->with($event);
+ } else {
+ $this->activityManager->expects($this->never())
+ ->method('generateEvent');
+ }
+
+ $this->invokePrivate($backend, 'triggerCardActivity', [$action, $addressBookData, $shares, $cardData]);
+ }
+
+ public function testNoCardActivityCreatedForSystemAddressbook(): void {
+ $backend = $this->getBackend();
+ $this->activityManager->expects($this->never())
+ ->method('generateEvent');
+ $this->assertEmpty($this->invokePrivate($backend, 'triggerCardActivity', [Card::SUBJECT_UPDATE, ['principaluri' => 'principals/system/system'], [], []]));
+ }
+
+ public static function dataGetUsersForShares(): array {
+ return [
+ [
+ [],
+ [],
+ [],
+ ],
+ [
+ [
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user1'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user3'],
+ ],
+ [],
+ ['user1', 'user2', 'user3'],
+ ],
+ [
+ [
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user1'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'],
+ ],
+ ['group2' => null, 'group3' => null],
+ ['user1', 'user2'],
+ ],
+ [
+ [
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user1'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'],
+ ],
+ ['group2' => ['user1', 'user2', 'user3'], 'group3' => ['user2', 'user3', 'user4']],
+ ['user1', 'user2', 'user3', 'user4'],
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGetUsersForShares')]
+ public function testGetUsersForShares(array $shares, array $groups, array $expected): void {
+ $backend = $this->getBackend();
+
+ $getGroups = [];
+ foreach ($groups as $gid => $members) {
+ if ($members === null) {
+ $getGroups[] = [$gid, null];
+ continue;
+ }
+
+ $group = $this->createMock(IGroup::class);
+ $group->expects($this->once())
+ ->method('getUsers')
+ ->willReturn($this->getUsers($members));
+
+ $getGroups[] = [$gid, $group];
+ }
+
+ $this->groupManager->expects($this->exactly(sizeof($getGroups)))
+ ->method('get')
+ ->willReturnMap($getGroups);
+
+ $users = $this->invokePrivate($backend, 'getUsersForShares', [$shares]);
+ sort($users);
+ $this->assertEquals($expected, $users);
+ }
+
+ /**
+ * @param string[] $users
+ * @return IUser[]|MockObject[]
+ */
+ protected function getUsers(array $users): array {
+ $list = [];
+ foreach ($users as $user) {
+ $list[] = $this->getUserMock($user);
+ }
+ return $list;
+ }
+
+ /**
+ * @return IUser|MockObject
+ */
+ protected function getUserMock(string $uid): IUser {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())
+ ->method('getUID')
+ ->willReturn($uid);
+ return $user;
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php
new file mode 100644
index 00000000000..74699cf3925
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php
@@ -0,0 +1,537 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CardDAV;
+
+use OCA\DAV\CardDAV\AddressBook;
+use OCA\DAV\CardDAV\AddressBookImpl;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\Db\PropertyMapper;
+use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCard;
+use Sabre\VObject\Property\Text;
+//use Sabre\VObject\Property\;
+use Test\TestCase;
+
+class AddressBookImplTest extends TestCase {
+ private array $addressBookInfo;
+ private AddressBook&MockObject $addressBook;
+ private IURLGenerator&MockObject $urlGenerator;
+ private CardDavBackend&MockObject $backend;
+ private PropertyMapper&MockObject $propertyMapper;
+ private VCard&MockObject $vCard;
+ private AddressBookImpl $addressBookImpl;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->addressBookInfo = [
+ 'id' => 42,
+ 'uri' => 'system',
+ 'principaluri' => 'principals/system/system',
+ '{DAV:}displayname' => 'display name',
+ ];
+ $this->addressBook = $this->createMock(AddressBook::class);
+ $this->backend = $this->createMock(CardDavBackend::class);
+ $this->vCard = $this->createMock(VCard::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->propertyMapper = $this->createMock(PropertyMapper::class);
+
+ $this->addressBookImpl = new AddressBookImpl(
+ $this->addressBook,
+ $this->addressBookInfo,
+ $this->backend,
+ $this->urlGenerator,
+ $this->propertyMapper,
+ null
+ );
+ }
+
+ public function testGetKey(): void {
+ $this->assertSame($this->addressBookInfo['id'],
+ $this->addressBookImpl->getKey());
+ }
+
+ public function testGetDisplayName(): void {
+ $this->assertSame($this->addressBookInfo['{DAV:}displayname'],
+ $this->addressBookImpl->getDisplayName());
+ }
+
+ public function testSearch(): void {
+ /** @var MockObject&AddressBookImpl $addressBookImpl */
+ $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class)
+ ->setConstructorArgs(
+ [
+ $this->addressBook,
+ $this->addressBookInfo,
+ $this->backend,
+ $this->urlGenerator,
+ $this->propertyMapper,
+ null
+ ]
+ )
+ ->onlyMethods(['vCard2Array', 'readCard'])
+ ->getMock();
+
+ $pattern = 'pattern';
+ $searchProperties = 'properties';
+
+ $this->backend->expects($this->once())->method('search')
+ ->with($this->addressBookInfo['id'], $pattern, $searchProperties)
+ ->willReturn(
+ [
+ ['uri' => 'foo.vcf', 'carddata' => 'cardData1'],
+ ['uri' => 'bar.vcf', 'carddata' => 'cardData2']
+ ]
+ );
+
+ $addressBookImpl->expects($this->exactly(2))->method('readCard')
+ ->willReturn($this->vCard);
+ $addressBookImpl->expects($this->exactly(2))->method('vCard2Array')
+ ->willReturnMap([
+ ['foo.vcf', $this->vCard, 'vCard'],
+ ['bar.vcf', $this->vCard, 'vCard'],
+ ]);
+
+ $result = $addressBookImpl->search($pattern, $searchProperties, []);
+ $this->assertTrue((is_array($result)));
+ $this->assertSame(2, count($result));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCreate')]
+ public function testCreate(array $properties): void {
+ $uid = 'uid';
+
+ /** @var MockObject&AddressBookImpl $addressBookImpl */
+ $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class)
+ ->setConstructorArgs(
+ [
+ $this->addressBook,
+ $this->addressBookInfo,
+ $this->backend,
+ $this->urlGenerator,
+ $this->propertyMapper,
+ null
+ ]
+ )
+ ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard'])
+ ->getMock();
+
+ $expectedProperties = 0;
+ foreach ($properties as $data) {
+ if (is_string($data)) {
+ $expectedProperties++;
+ } else {
+ $expectedProperties += count($data);
+ }
+ }
+
+ $addressBookImpl->expects($this->once())->method('createUid')
+ ->willReturn($uid);
+ $addressBookImpl->expects($this->once())->method('createEmptyVCard')
+ ->with($uid)->willReturn($this->vCard);
+ $this->vCard->expects($this->exactly($expectedProperties))
+ ->method('createProperty');
+ $this->backend->expects($this->once())->method('createCard');
+ $this->backend->expects($this->never())->method('updateCard');
+ $this->backend->expects($this->never())->method('getCard');
+ $addressBookImpl->expects($this->once())->method('vCard2Array')
+ ->with('uid.vcf', $this->vCard)->willReturn(true);
+
+ $this->assertTrue($addressBookImpl->createOrUpdate($properties));
+ }
+
+ public static function dataTestCreate(): array {
+ return [
+ [[]],
+ [['FN' => 'John Doe']],
+ [['FN' => 'John Doe', 'EMAIL' => ['john@doe.cloud', 'john.doe@example.org']]],
+ ];
+ }
+
+ public function testUpdate(): void {
+ $uid = 'uid';
+ $uri = 'bla.vcf';
+ $properties = ['URI' => $uri, 'UID' => $uid, 'FN' => 'John Doe'];
+
+ /** @var MockObject&AddressBookImpl $addressBookImpl */
+ $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class)
+ ->setConstructorArgs(
+ [
+ $this->addressBook,
+ $this->addressBookInfo,
+ $this->backend,
+ $this->urlGenerator,
+ $this->propertyMapper,
+ null
+ ]
+ )
+ ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard'])
+ ->getMock();
+
+ $addressBookImpl->expects($this->never())->method('createUid');
+ $addressBookImpl->expects($this->never())->method('createEmptyVCard');
+ $this->backend->expects($this->once())->method('getCard')
+ ->with($this->addressBookInfo['id'], $uri)
+ ->willReturn(['carddata' => 'data']);
+ $addressBookImpl->expects($this->once())->method('readCard')
+ ->with('data')->willReturn($this->vCard);
+ $this->vCard->expects($this->exactly(count($properties) - 1))
+ ->method('createProperty');
+ $this->backend->expects($this->never())->method('createCard');
+ $this->backend->expects($this->once())->method('updateCard');
+ $addressBookImpl->expects($this->once())->method('vCard2Array')
+ ->with($uri, $this->vCard)->willReturn(true);
+
+ $this->assertTrue($addressBookImpl->createOrUpdate($properties));
+ }
+
+ public function testUpdateWithTypes(): void {
+ $uid = 'uid';
+ $uri = 'bla.vcf';
+ $properties = ['URI' => $uri, 'UID' => $uid, 'FN' => 'John Doe', 'ADR' => [['type' => 'HOME', 'value' => ';;street;city;;;country']]];
+ $vCard = new vCard;
+ $textProperty = $vCard->createProperty('KEY', 'value');
+
+ /** @var MockObject&AddressBookImpl $addressBookImpl */
+ $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class)
+ ->setConstructorArgs(
+ [
+ $this->addressBook,
+ $this->addressBookInfo,
+ $this->backend,
+ $this->urlGenerator,
+ $this->propertyMapper,
+ null
+ ]
+ )
+ ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard'])
+ ->getMock();
+
+ $this->backend->expects($this->once())->method('getCard')
+ ->with($this->addressBookInfo['id'], $uri)
+ ->willReturn(['carddata' => 'data']);
+ $addressBookImpl->expects($this->once())->method('readCard')
+ ->with('data')->willReturn($this->vCard);
+ $this->vCard->method('createProperty')->willReturn($textProperty);
+ $this->vCard->expects($this->exactly(count($properties) - 1))
+ ->method('createProperty');
+ $this->vCard->expects($this->once())->method('remove')
+ ->with('ADR');
+ $this->vCard->expects($this->once())->method('add');
+
+ $addressBookImpl->createOrUpdate($properties);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetPermissions')]
+ public function testGetPermissions(array $permissions, int $expected): void {
+ $this->addressBook->expects($this->once())->method('getACL')
+ ->willReturn($permissions);
+
+ $this->assertSame($expected,
+ $this->addressBookImpl->getPermissions()
+ );
+ }
+
+ public static function dataTestGetPermissions(): array {
+ return [
+ [[], 0],
+ [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system']], 1],
+ [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'], ['privilege' => '{DAV:}write', 'principal' => 'principals/someone/else']], 1],
+ [[['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 6],
+ [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31],
+ [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 7],
+ [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31],
+ [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 31],
+ [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31],
+ [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 31],
+ ];
+ }
+
+ public function testDelete(): void {
+ $cardId = 1;
+ $cardUri = 'cardUri';
+ $this->backend->expects($this->once())->method('getCardUri')
+ ->with($cardId)->willReturn($cardUri);
+ $this->backend->expects($this->once())->method('deleteCard')
+ ->with($this->addressBookInfo['id'], $cardUri)
+ ->willReturn(true);
+
+ $this->assertTrue($this->addressBookImpl->delete($cardId));
+ }
+
+ public function testReadCard(): void {
+ $vCard = new VCard();
+ $vCard->add(new Text($vCard, 'UID', 'uid'));
+ $vCardSerialized = $vCard->serialize();
+
+ $result = $this->invokePrivate($this->addressBookImpl, 'readCard', [$vCardSerialized]);
+ $resultSerialized = $result->serialize();
+
+ $this->assertSame($vCardSerialized, $resultSerialized);
+ }
+
+ public function testCreateUid(): void {
+ /** @var MockObject&AddressBookImpl $addressBookImpl */
+ $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class)
+ ->setConstructorArgs(
+ [
+ $this->addressBook,
+ $this->addressBookInfo,
+ $this->backend,
+ $this->urlGenerator,
+ $this->propertyMapper,
+ null
+ ]
+ )
+ ->onlyMethods(['getUid'])
+ ->getMock();
+
+ $addressBookImpl->expects($this->exactly(2))
+ ->method('getUid')
+ ->willReturnOnConsecutiveCalls(
+ 'uid0',
+ 'uid1',
+ );
+
+ // simulate that 'uid0' already exists, so the second uid will be returned
+ $this->backend->expects($this->exactly(2))->method('getContact')
+ ->willReturnCallback(
+ function ($id, $uid) {
+ return ($uid === 'uid0.vcf');
+ }
+ );
+
+ $this->assertSame('uid1',
+ $this->invokePrivate($addressBookImpl, 'createUid', [])
+ );
+ }
+
+ public function testCreateEmptyVCard(): void {
+ $uid = 'uid';
+ $expectedVCard = new VCard();
+ $expectedVCard->UID = $uid;
+ $expectedVCardSerialized = $expectedVCard->serialize();
+
+ $result = $this->invokePrivate($this->addressBookImpl, 'createEmptyVCard', [$uid]);
+ $resultSerialized = $result->serialize();
+
+ $this->assertSame($expectedVCardSerialized, $resultSerialized);
+ }
+
+ public function testVCard2Array(): void {
+ $vCard = new VCard();
+
+ $vCard->add($vCard->createProperty('FN', 'Full Name'));
+
+ // Multi-value properties
+ $vCard->add($vCard->createProperty('CLOUD', 'cloud-user1@localhost'));
+ $vCard->add($vCard->createProperty('CLOUD', 'cloud-user2@example.tld'));
+ $vCard->add($vCard->createProperty('EMAIL', 'email-user1@localhost'));
+ $vCard->add($vCard->createProperty('EMAIL', 'email-user2@example.tld'));
+ $vCard->add($vCard->createProperty('IMPP', 'impp-user1@localhost'));
+ $vCard->add($vCard->createProperty('IMPP', 'impp-user2@example.tld'));
+ $vCard->add($vCard->createProperty('TEL', '+49 123456789'));
+ $vCard->add($vCard->createProperty('TEL', '+1 555 123456789'));
+ $vCard->add($vCard->createProperty('URL', 'https://localhost'));
+ $vCard->add($vCard->createProperty('URL', 'https://example.tld'));
+
+ // Type depending properties
+ $property = $vCard->createProperty('X-SOCIALPROFILE', 'tw-example');
+ $property->add('TYPE', 'twitter');
+ $vCard->add($property);
+ $property = $vCard->createProperty('X-SOCIALPROFILE', 'tw-example-2');
+ $property->add('TYPE', 'twitter');
+ $vCard->add($property);
+ $property = $vCard->createProperty('X-SOCIALPROFILE', 'fb-example');
+ $property->add('TYPE', 'facebook');
+ $vCard->add($property);
+
+ $array = $this->invokePrivate($this->addressBookImpl, 'vCard2Array', ['uri', $vCard]);
+ unset($array['PRODID']);
+ unset($array['UID']);
+
+ $this->assertEquals([
+ 'URI' => 'uri',
+ 'VERSION' => '4.0',
+ 'FN' => 'Full Name',
+ 'CLOUD' => [
+ 'cloud-user1@localhost',
+ 'cloud-user2@example.tld',
+ ],
+ 'EMAIL' => [
+ 'email-user1@localhost',
+ 'email-user2@example.tld',
+ ],
+ 'IMPP' => [
+ 'impp-user1@localhost',
+ 'impp-user2@example.tld',
+ ],
+ 'TEL' => [
+ '+49 123456789',
+ '+1 555 123456789',
+ ],
+ 'URL' => [
+ 'https://localhost',
+ 'https://example.tld',
+ ],
+
+ 'X-SOCIALPROFILE' => [
+ 'tw-example',
+ 'tw-example-2',
+ 'fb-example',
+ ],
+
+ 'isLocalSystemBook' => true,
+ ], $array);
+ }
+
+ public function testVCard2ArrayWithTypes(): void {
+ $vCard = new VCard();
+
+ $vCard->add($vCard->createProperty('FN', 'Full Name'));
+
+ // Multi-value properties
+ $vCard->add($vCard->createProperty('CLOUD', 'cloud-user1@localhost'));
+ $vCard->add($vCard->createProperty('CLOUD', 'cloud-user2@example.tld'));
+
+ $property = $vCard->createProperty('EMAIL', 'email-user1@localhost');
+ $property->add('TYPE', 'HOME');
+ $vCard->add($property);
+ $property = $vCard->createProperty('EMAIL', 'email-user2@example.tld');
+ $property->add('TYPE', 'WORK');
+ $vCard->add($property);
+
+ $vCard->add($vCard->createProperty('IMPP', 'impp-user1@localhost'));
+ $vCard->add($vCard->createProperty('IMPP', 'impp-user2@example.tld'));
+
+ $property = $vCard->createProperty('TEL', '+49 123456789');
+ $property->add('TYPE', 'HOME,VOICE');
+ $vCard->add($property);
+ $property = $vCard->createProperty('TEL', '+1 555 123456789');
+ $property->add('TYPE', 'WORK');
+ $vCard->add($property);
+
+ $vCard->add($vCard->createProperty('URL', 'https://localhost'));
+ $vCard->add($vCard->createProperty('URL', 'https://example.tld'));
+
+ // Type depending properties
+ $property = $vCard->createProperty('X-SOCIALPROFILE', 'tw-example');
+ $property->add('TYPE', 'twitter');
+ $vCard->add($property);
+ $property = $vCard->createProperty('X-SOCIALPROFILE', 'tw-example-2');
+ $property->add('TYPE', 'twitter');
+ $vCard->add($property);
+ $property = $vCard->createProperty('X-SOCIALPROFILE', 'fb-example');
+ $property->add('TYPE', 'facebook');
+ $vCard->add($property);
+
+ $array = $this->invokePrivate($this->addressBookImpl, 'vCard2Array', ['uri', $vCard, true]);
+ unset($array['PRODID']);
+ unset($array['UID']);
+
+ $this->assertEquals([
+ 'URI' => 'uri',
+ 'VERSION' => '4.0',
+ 'FN' => 'Full Name',
+ 'CLOUD' => [
+ ['type' => '', 'value' => 'cloud-user1@localhost'],
+ ['type' => '', 'value' => 'cloud-user2@example.tld'],
+ ],
+ 'EMAIL' => [
+ ['type' => 'HOME', 'value' => 'email-user1@localhost'],
+ ['type' => 'WORK', 'value' => 'email-user2@example.tld'],
+ ],
+ 'IMPP' => [
+ ['type' => '', 'value' => 'impp-user1@localhost'],
+ ['type' => '', 'value' => 'impp-user2@example.tld'],
+ ],
+ 'TEL' => [
+ ['type' => 'HOME,VOICE', 'value' => '+49 123456789'],
+ ['type' => 'WORK', 'value' => '+1 555 123456789'],
+ ],
+ 'URL' => [
+ ['type' => '', 'value' => 'https://localhost'],
+ ['type' => '', 'value' => 'https://example.tld'],
+ ],
+
+ 'X-SOCIALPROFILE' => [
+ ['type' => 'twitter', 'value' => 'tw-example'],
+ ['type' => 'twitter', 'value' => 'tw-example-2'],
+ ['type' => 'facebook', 'value' => 'fb-example'],
+ ],
+
+ 'isLocalSystemBook' => true,
+ ], $array);
+ }
+
+ public function testIsSystemAddressBook(): void {
+ $addressBookInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'principals/system/system',
+ 'principaluri' => 'principals/system/system',
+ '{DAV:}displayname' => 'display name',
+ 'id' => 666,
+ 'uri' => 'system',
+ ];
+
+ $addressBookImpl = new AddressBookImpl(
+ $this->addressBook,
+ $addressBookInfo,
+ $this->backend,
+ $this->urlGenerator,
+ $this->propertyMapper,
+ null
+ );
+
+ $this->assertTrue($addressBookImpl->isSystemAddressBook());
+ }
+
+ public function testIsShared(): void {
+ $addressBookInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ '{DAV:}displayname' => 'Test address book',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'default',
+ ];
+
+ $addressBookImpl = new AddressBookImpl(
+ $this->addressBook,
+ $addressBookInfo,
+ $this->backend,
+ $this->urlGenerator,
+ $this->propertyMapper,
+ 'user2'
+ );
+
+ $this->assertFalse($addressBookImpl->isSystemAddressBook());
+ $this->assertTrue($addressBookImpl->isShared());
+ }
+
+ public function testIsNotShared(): void {
+ $addressBookInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ '{DAV:}displayname' => 'Test address book',
+ 'principaluri' => 'user1',
+ 'id' => 666,
+ 'uri' => 'default',
+ ];
+
+ $addressBookImpl = new AddressBookImpl(
+ $this->addressBook,
+ $addressBookInfo,
+ $this->backend,
+ $this->urlGenerator,
+ $this->propertyMapper,
+ 'user2'
+ );
+
+ $this->assertFalse($addressBookImpl->isSystemAddressBook());
+ $this->assertFalse($addressBookImpl->isShared());
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/AddressBookTest.php b/apps/dav/tests/unit/CardDAV/AddressBookTest.php
new file mode 100644
index 00000000000..cf28b7b8a8e
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/AddressBookTest.php
@@ -0,0 +1,184 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CardDAV;
+
+use OCA\DAV\CardDAV\AddressBook;
+use OCA\DAV\CardDAV\Card;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCP\IL10N;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\PropPatch;
+use Test\TestCase;
+
+class AddressBookTest extends TestCase {
+ public function testMove(): void {
+ $backend = $this->createMock(CardDavBackend::class);
+ $addressBookInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ '{DAV:}displayname' => 'Test address book',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'default',
+ ];
+ $l10n = $this->createMock(IL10N::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+
+ $card = new Card($backend, $addressBookInfo, ['id' => 5, 'carddata' => 'RANDOM VCF DATA', 'uri' => 'something', 'addressbookid' => 23]);
+
+ $backend->expects($this->once())->method('moveCard')
+ ->with(23, 'something', 666, 'new')
+ ->willReturn(true);
+
+ $addressBook->moveInto('new', 'old', $card);
+ }
+
+ public function testDelete(): void {
+ /** @var MockObject | CardDavBackend $backend */
+ $backend = $this->createMock(CardDavBackend::class);
+ $backend->expects($this->once())->method('updateShares');
+ $backend->expects($this->any())->method('getShares')->willReturn([
+ ['href' => 'principal:user2']
+ ]);
+ $addressBookInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ '{DAV:}displayname' => 'Test address book',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'default',
+ ];
+ $l10n = $this->createMock(IL10N::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+ $addressBook->delete();
+ }
+
+
+ public function testDeleteFromGroup(): void {
+ $this->expectException(Forbidden::class);
+
+ /** @var MockObject | CardDavBackend $backend */
+ $backend = $this->createMock(CardDavBackend::class);
+ $backend->expects($this->never())->method('updateShares');
+ $backend->expects($this->any())->method('getShares')->willReturn([
+ ['href' => 'principal:group2']
+ ]);
+ $addressBookInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ '{DAV:}displayname' => 'Test address book',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'default',
+ ];
+ $l10n = $this->createMock(IL10N::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+ $addressBook->delete();
+ }
+
+
+ public function testPropPatchShared(): void {
+ /** @var MockObject | CardDavBackend $backend */
+ $backend = $this->createMock(CardDavBackend::class);
+ $backend->expects($this->never())->method('updateAddressBook');
+ $addressBookInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ '{DAV:}displayname' => 'Test address book',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'default',
+ ];
+ $l10n = $this->createMock(IL10N::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+ $addressBook->propPatch(new PropPatch(['{DAV:}displayname' => 'Test address book']));
+ }
+
+ public function testPropPatchNotShared(): void {
+ /** @var MockObject | CardDavBackend $backend */
+ $backend = $this->createMock(CardDavBackend::class);
+ $backend->expects($this->atLeast(1))->method('updateAddressBook');
+ $addressBookInfo = [
+ '{DAV:}displayname' => 'Test address book',
+ 'principaluri' => 'user1',
+ 'id' => 666,
+ 'uri' => 'default',
+ ];
+ $l10n = $this->createMock(IL10N::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+ $addressBook->propPatch(new PropPatch(['{DAV:}displayname' => 'Test address book']));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesReadOnlyInfo')]
+ public function testAcl(bool $expectsWrite, ?bool $readOnlyValue, bool $hasOwnerSet): void {
+ /** @var MockObject | CardDavBackend $backend */
+ $backend = $this->createMock(CardDavBackend::class);
+ $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1);
+ $addressBookInfo = [
+ '{DAV:}displayname' => 'Test address book',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'default'
+ ];
+ if (!is_null($readOnlyValue)) {
+ $addressBookInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue;
+ }
+ if ($hasOwnerSet) {
+ $addressBookInfo['{http://owncloud.org/ns}owner-principal'] = 'user1';
+ }
+ $l10n = $this->createMock(IL10N::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+ $acl = $addressBook->getACL();
+ $childAcl = $addressBook->getChildACL();
+
+ $expectedAcl = [[
+ 'privilege' => '{DAV:}read',
+ 'principal' => $hasOwnerSet ? 'user1' : 'user2',
+ 'protected' => true
+ ], [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $hasOwnerSet ? 'user1' : 'user2',
+ 'protected' => true
+ ], [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $hasOwnerSet ? 'user1' : 'user2',
+ 'protected' => true
+ ]];
+ if ($hasOwnerSet) {
+ $expectedAcl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'user2',
+ 'protected' => true
+ ];
+ if ($expectsWrite) {
+ $expectedAcl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => 'user2',
+ 'protected' => true
+ ];
+ }
+ }
+ $this->assertEquals($expectedAcl, $acl);
+ $this->assertEquals($expectedAcl, $childAcl);
+ }
+
+ public static function providesReadOnlyInfo(): array {
+ return [
+ 'read-only property not set' => [true, null, true],
+ 'read-only property is false' => [true, false, true],
+ 'read-only property is true' => [false, true, true],
+ 'read-only property not set and no owner' => [true, null, false],
+ 'read-only property is false and no owner' => [true, false, false],
+ 'read-only property is true and no owner' => [false, true, false],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php b/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php
new file mode 100644
index 00000000000..6908dfd17bc
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php
@@ -0,0 +1,433 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CardDAV;
+
+use OCA\DAV\CalDAV\BirthdayService;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\DAV\GroupPrincipalBackend;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IL10N;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+class BirthdayServiceTest extends TestCase {
+ private CalDavBackend&MockObject $calDav;
+ private CardDavBackend&MockObject $cardDav;
+ private GroupPrincipalBackend&MockObject $groupPrincipalBackend;
+ private IConfig&MockObject $config;
+ private IDBConnection&MockObject $dbConnection;
+ private IL10N&MockObject $l10n;
+ private BirthdayService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calDav = $this->createMock(CalDavBackend::class);
+ $this->cardDav = $this->createMock(CardDavBackend::class);
+ $this->groupPrincipalBackend = $this->createMock(GroupPrincipalBackend::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->dbConnection = $this->createMock(IDBConnection::class);
+ $this->l10n = $this->createMock(IL10N::class);
+
+ $this->l10n->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($string, $args) {
+ return vsprintf($string, $args);
+ });
+
+ $this->service = new BirthdayService($this->calDav, $this->cardDav,
+ $this->groupPrincipalBackend, $this->config,
+ $this->dbConnection, $this->l10n);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesVCards')]
+ public function testBuildBirthdayFromContact(?string $expectedSummary, ?string $expectedDTStart, ?string $expectedRrule, ?string $expectedFieldType, ?string $expectedUnknownYear, ?string $expectedOriginalYear, ?string $expectedReminder, ?string $data, string $fieldType, string $prefix, bool $supports4Bytes, ?string $configuredReminder): void {
+ $this->dbConnection->method('supports4ByteText')->willReturn($supports4Bytes);
+ $cal = $this->service->buildDateFromContact($data, $fieldType, $prefix, $configuredReminder);
+
+ if ($expectedSummary === null) {
+ $this->assertNull($cal);
+ } else {
+ $this->assertInstanceOf('Sabre\VObject\Component\VCalendar', $cal);
+ $this->assertEquals('-//IDN nextcloud.com//Birthday calendar//EN', $cal->PRODID->getValue());
+ $this->assertTrue(isset($cal->VEVENT));
+ $this->assertEquals($expectedRrule, $cal->VEVENT->RRULE->getValue());
+ $this->assertEquals($expectedSummary, $cal->VEVENT->SUMMARY->getValue());
+ $this->assertEquals($expectedDTStart, $cal->VEVENT->DTSTART->getValue());
+ $this->assertEquals($expectedFieldType, $cal->VEVENT->{'X-NEXTCLOUD-BC-FIELD-TYPE'}->getValue());
+ $this->assertEquals($expectedUnknownYear, $cal->VEVENT->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'}->getValue());
+
+ if ($expectedOriginalYear) {
+ $this->assertEquals($expectedOriginalYear, $cal->VEVENT->{'X-NEXTCLOUD-BC-YEAR'}->getValue());
+ }
+
+ if ($expectedReminder) {
+ $this->assertEquals($expectedReminder, $cal->VEVENT->VALARM->TRIGGER->getValue());
+ $this->assertEquals('DURATION', $cal->VEVENT->VALARM->TRIGGER->getValueType());
+ }
+
+ $this->assertEquals('TRANSPARENT', $cal->VEVENT->TRANSP->getValue());
+ }
+ }
+
+ public function testOnCardDeleteGloballyDisabled(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('no');
+
+ $this->cardDav->expects($this->never())->method('getAddressBookById');
+
+ $this->service->onCardDeleted(666, 'gump.vcf');
+ }
+
+ public function testOnCardDeleteUserDisabled(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('yes');
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('user01', 'dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('no');
+
+ $this->cardDav->expects($this->once())->method('getAddressBookById')
+ ->with(666)
+ ->willReturn([
+ 'principaluri' => 'principals/users/user01',
+ 'uri' => 'default'
+ ]);
+ $this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
+ $this->calDav->expects($this->never())->method('getCalendarByUri');
+ $this->calDav->expects($this->never())->method('deleteCalendarObject');
+
+ $this->service->onCardDeleted(666, 'gump.vcf');
+ }
+
+ public function testOnCardDeleted(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('yes');
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('user01', 'dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('yes');
+
+ $this->cardDav->expects($this->once())->method('getAddressBookById')
+ ->with(666)
+ ->willReturn([
+ 'principaluri' => 'principals/users/user01',
+ 'uri' => 'default'
+ ]);
+ $this->calDav->expects($this->once())->method('getCalendarByUri')
+ ->with('principals/users/user01', 'contact_birthdays')
+ ->willReturn([
+ 'id' => 1234
+ ]);
+ $calls = [
+ [1234, 'default-gump.vcf.ics'],
+ [1234, 'default-gump.vcf-death.ics'],
+ [1234, 'default-gump.vcf-anniversary.ics'],
+ ];
+ $this->calDav->expects($this->exactly(count($calls)))
+ ->method('deleteCalendarObject')
+ ->willReturnCallback(function ($calendarId, $objectUri) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, [$calendarId, $objectUri]);
+ });
+ $this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
+
+ $this->service->onCardDeleted(666, 'gump.vcf');
+ }
+
+ public function testOnCardChangedGloballyDisabled(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('no');
+
+ $this->cardDav->expects($this->never())->method('getAddressBookById');
+
+ $service = $this->getMockBuilder(BirthdayService::class)
+ ->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged'])
+ ->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n])
+ ->getMock();
+
+ $service->onCardChanged(666, 'gump.vcf', '');
+ }
+
+ public function testOnCardChangedUserDisabled(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('yes');
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('user01', 'dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('no');
+
+ $this->cardDav->expects($this->once())->method('getAddressBookById')
+ ->with(666)
+ ->willReturn([
+ 'principaluri' => 'principals/users/user01',
+ 'uri' => 'default'
+ ]);
+ $this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
+ $this->calDav->expects($this->never())->method('getCalendarByUri');
+
+ /** @var BirthdayService&MockObject $service */
+ $service = $this->getMockBuilder(BirthdayService::class)
+ ->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged'])
+ ->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n])
+ ->getMock();
+
+ $service->onCardChanged(666, 'gump.vcf', '');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesCardChanges')]
+ public function testOnCardChanged(string $expectedOp): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes')
+ ->willReturn('yes');
+
+ $this->config->expects($this->exactly(2))
+ ->method('getUserValue')
+ ->willReturnMap([
+ ['user01', 'dav', 'generateBirthdayCalendar', 'yes', 'yes'],
+ ['user01', 'dav', 'birthdayCalendarReminderOffset', 'PT9H', 'PT9H'],
+ ]);
+
+ $this->cardDav->expects($this->once())->method('getAddressBookById')
+ ->with(666)
+ ->willReturn([
+ 'principaluri' => 'principals/users/user01',
+ 'uri' => 'default'
+ ]);
+ $this->calDav->expects($this->once())->method('getCalendarByUri')
+ ->with('principals/users/user01', 'contact_birthdays')
+ ->willReturn([
+ 'id' => 1234
+ ]);
+ $this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
+
+ /** @var BirthdayService&MockObject $service */
+ $service = $this->getMockBuilder(BirthdayService::class)
+ ->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged'])
+ ->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n])
+ ->getMock();
+
+ if ($expectedOp === 'delete') {
+ $this->calDav->expects($this->exactly(3))->method('getCalendarObject')->willReturn('');
+ $service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn(null);
+
+ $calls = [
+ [1234, 'default-gump.vcf.ics'],
+ [1234, 'default-gump.vcf-death.ics'],
+ [1234, 'default-gump.vcf-anniversary.ics']
+ ];
+ $this->calDav->expects($this->exactly(count($calls)))
+ ->method('deleteCalendarObject')
+ ->willReturnCallback(function ($calendarId, $objectUri) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, [$calendarId, $objectUri]);
+ });
+ }
+ if ($expectedOp === 'create') {
+ $vCal = new VCalendar();
+ $vCal->PRODID = '-//Nextcloud testing//mocked object//';
+
+ $service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn($vCal);
+
+ $createCalendarObjectCalls = [
+ [1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
+ [1234, 'default-gump.vcf-death.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
+ [1234, 'default-gump.vcf-anniversary.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"]
+ ];
+ $this->calDav->expects($this->exactly(count($createCalendarObjectCalls)))
+ ->method('createCalendarObject')
+ ->willReturnCallback(function ($calendarId, $objectUri, $calendarData) use (&$createCalendarObjectCalls): void {
+ $expected = array_shift($createCalendarObjectCalls);
+ $this->assertEquals($expected, [$calendarId, $objectUri, $calendarData]);
+ });
+ }
+ if ($expectedOp === 'update') {
+ $vCal = new VCalendar();
+ $vCal->PRODID = '-//Nextcloud testing//mocked object//';
+
+ $service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn($vCal);
+ $service->expects($this->exactly(3))->method('birthdayEvenChanged')->willReturn(true);
+ $this->calDav->expects($this->exactly(3))->method('getCalendarObject')->willReturn(['calendardata' => '']);
+
+ $updateCalendarObjectCalls = [
+ [1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
+ [1234, 'default-gump.vcf-death.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
+ [1234, 'default-gump.vcf-anniversary.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"]
+ ];
+ $this->calDav->expects($this->exactly(count($updateCalendarObjectCalls)))
+ ->method('updateCalendarObject')
+ ->willReturnCallback(function ($calendarId, $objectUri, $calendarData) use (&$updateCalendarObjectCalls): void {
+ $expected = array_shift($updateCalendarObjectCalls);
+ $this->assertEquals($expected, [$calendarId, $objectUri, $calendarData]);
+ });
+ }
+
+ $service->onCardChanged(666, 'gump.vcf', '');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesBirthday')]
+ public function testBirthdayEvenChanged(bool $expected, string $old, string $new): void {
+ $new = Reader::read($new);
+ $this->assertEquals($expected, $this->service->birthdayEvenChanged($old, $new));
+ }
+
+ public function testGetAllAffectedPrincipals(): void {
+ $this->cardDav->expects($this->once())->method('getShares')->willReturn([
+ [
+ '{http://owncloud.org/ns}group-share' => false,
+ '{http://owncloud.org/ns}principal' => 'principals/users/user01'
+ ],
+ [
+ '{http://owncloud.org/ns}group-share' => false,
+ '{http://owncloud.org/ns}principal' => 'principals/users/user01'
+ ],
+ [
+ '{http://owncloud.org/ns}group-share' => false,
+ '{http://owncloud.org/ns}principal' => 'principals/users/user02'
+ ],
+ [
+ '{http://owncloud.org/ns}group-share' => true,
+ '{http://owncloud.org/ns}principal' => 'principals/groups/users'
+ ],
+ ]);
+ $this->groupPrincipalBackend->expects($this->once())->method('getGroupMemberSet')
+ ->willReturn([
+ [
+ 'uri' => 'principals/users/user01',
+ ],
+ [
+ 'uri' => 'principals/users/user02',
+ ],
+ [
+ 'uri' => 'principals/users/user03',
+ ],
+ ]);
+ $users = $this->invokePrivate($this->service, 'getAllAffectedPrincipals', [6666]);
+ $this->assertEquals([
+ 'principals/users/user01',
+ 'principals/users/user02',
+ 'principals/users/user03'
+ ], $users);
+ }
+
+ public function testBirthdayCalendarHasComponentEvent(): void {
+ $this->calDav->expects($this->once())
+ ->method('createCalendar')
+ ->with('principal001', 'contact_birthdays', [
+ '{DAV:}displayname' => 'Contact birthdays',
+ '{http://apple.com/ns/ical/}calendar-color' => '#E9D859',
+ 'components' => 'VEVENT',
+ ]);
+ $this->service->ensureCalendarExists('principal001');
+ }
+
+ public function testResetForUser(): void {
+ $this->calDav->expects($this->once())
+ ->method('getCalendarByUri')
+ ->with('principals/users/user123', 'contact_birthdays')
+ ->willReturn(['id' => 42]);
+
+ $this->calDav->expects($this->once())
+ ->method('getCalendarObjects')
+ ->with(42, 0)
+ ->willReturn([['uri' => '1.ics'], ['uri' => '2.ics'], ['uri' => '3.ics']]);
+
+ $calls = [
+ [42, '1.ics', 0],
+ [42, '2.ics', 0],
+ [42, '3.ics', 0],
+ ];
+ $this->calDav->expects($this->exactly(count($calls)))
+ ->method('deleteCalendarObject')
+ ->willReturnCallback(function ($calendarId, $objectUri, $calendarType) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, [$calendarId, $objectUri, $calendarType]);
+ });
+
+ $this->service->resetForUser('user123');
+ }
+
+ public static function providesBirthday(): array {
+ return [
+ [true,
+ '',
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+ [false,
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+ [true,
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:4567's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+ [true,
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000102\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"]
+ ];
+ }
+
+ public static function providesCardChanges(): array {
+ return[
+ ['delete'],
+ ['create'],
+ ['update']
+ ];
+ }
+
+ public static function providesVCards(): array {
+ return [
+ // $expectedSummary, $expectedDTStart, $expectedRrule, $expectedFieldType, $expectedUnknownYear, $expectedOriginalYear, $expectedReminder, $data, $fieldType, $prefix, $supports4Byte, $configuredReminder
+ [null, null, null, null, null, null, null, 'yasfewf', '', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:someday\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['🎂 12345 (1900)', '19700101', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['🎂 12345 (1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['Death of 12345 (1900)', '19701231', 'FREQ=YEARLY', 'DEATHDATE', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', true, null],
+ ['Death of 12345 (1900)', '19701231', 'FREQ=YEARLY', 'DEATHDATE', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', false, null],
+ ['💍 12345 (1900)', '19701231', 'FREQ=YEARLY', 'ANNIVERSARY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', true, null],
+ ['12345 (⚭1900)', '19701231', 'FREQ=YEARLY', 'ANNIVERSARY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', false, null],
+ ['🎂 12345', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:--1231\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['🎂 12345', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY;X-APPLE-OMIT-YEAR=1604:16041231\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:;VALUE=text:circa 1800\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nN:12345;;;;\r\nBDAY:20031231\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['🎂 12345 (900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:09001231\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['12345 (*1900)', '19700101', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ ['12345 *', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:--1231\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ ['12345 *', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY;X-APPLE-OMIT-YEAR=1604:16041231\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:;VALUE=text:circa 1800\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nN:12345;;;;\r\nBDAY:20031231\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ ['12345 (*900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:09001231\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', 'PT9H', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, 'PT9H'],
+ ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', '-PT15H', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, '-PT15H'],
+ ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', '-P6DT15H', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, '-P6DT15H'],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nX-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR;TYPE=boolean:true\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nX-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR;TYPE=boolean:true\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nX-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR;TYPE=boolean:true\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', true, null],
+ ['🎂 12345 (1902)', '19720229', 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1', 'BDAY', '0', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19020229\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php
new file mode 100644
index 00000000000..c5eafa0764a
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php
@@ -0,0 +1,918 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CardDAV;
+
+use OC\KnownUser\KnownUserService;
+use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\CardDAV\AddressBook;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\Sharing\Backend;
+use OCA\DAV\CardDAV\Sharing\Service;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCP\Accounts\IAccountManager;
+use OCP\App\IAppManager;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\ICacheFactory;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Server;
+use OCP\Share\IManager as ShareManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\PropPatch;
+use Sabre\VObject\Component\VCard;
+use Sabre\VObject\Property\Text;
+use Test\TestCase;
+use function time;
+
+/**
+ * Class CardDavBackendTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\CardDAV
+ */
+class CardDavBackendTest extends TestCase {
+ private Principal&MockObject $principal;
+ private IUserManager&MockObject $userManager;
+ private IGroupManager&MockObject $groupManager;
+ private IEventDispatcher&MockObject $dispatcher;
+ private IConfig&MockObject $config;
+ private Backend $sharingBackend;
+ private IDBConnection $db;
+ private CardDavBackend $backend;
+ private string $dbCardsTable = 'cards';
+ private string $dbCardsPropertiesTable = 'cards_properties';
+
+ public const UNIT_TEST_USER = 'principals/users/carddav-unit-test';
+ public const UNIT_TEST_USER1 = 'principals/users/carddav-unit-test1';
+ public const UNIT_TEST_GROUP = 'principals/groups/carddav-unit-test-group';
+
+ private $vcardTest0 = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'UID:Test' . PHP_EOL
+ . 'FN:Test' . PHP_EOL
+ . 'N:Test;;;;' . PHP_EOL
+ . 'END:VCARD';
+
+ private $vcardTest1 = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'UID:Test2' . PHP_EOL
+ . 'FN:Test2' . PHP_EOL
+ . 'N:Test2;;;;' . PHP_EOL
+ . 'END:VCARD';
+
+ private $vcardTest2 = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'UID:Test3' . PHP_EOL
+ . 'FN:Test3' . PHP_EOL
+ . 'N:Test3;;;;' . PHP_EOL
+ . 'END:VCARD';
+
+ private $vcardTestNoUID = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'FN:TestNoUID' . PHP_EOL
+ . 'N:TestNoUID;;;;' . PHP_EOL
+ . 'END:VCARD';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->principal = $this->getMockBuilder(Principal::class)
+ ->setConstructorArgs([
+ $this->userManager,
+ $this->groupManager,
+ $this->createMock(IAccountManager::class),
+ $this->createMock(ShareManager::class),
+ $this->createMock(IUserSession::class),
+ $this->createMock(IAppManager::class),
+ $this->createMock(ProxyMapper::class),
+ $this->createMock(KnownUserService::class),
+ $this->config,
+ $this->createMock(IFactory::class)
+ ])
+ ->onlyMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri'])
+ ->getMock();
+ $this->principal->method('getPrincipalByPath')
+ ->willReturn([
+ 'uri' => 'principals/best-friend',
+ '{DAV:}displayname' => 'User\'s displayname',
+ ]);
+ $this->principal->method('getGroupMembership')
+ ->withAnyParameters()
+ ->willReturn([self::UNIT_TEST_GROUP]);
+ $this->dispatcher = $this->createMock(IEventDispatcher::class);
+
+ $this->db = Server::get(IDBConnection::class);
+ $this->sharingBackend = new Backend($this->userManager,
+ $this->groupManager,
+ $this->principal,
+ $this->createMock(ICacheFactory::class),
+ new Service(new SharingMapper($this->db)),
+ $this->createMock(LoggerInterface::class)
+ );
+
+ $this->backend = new CardDavBackend($this->db,
+ $this->principal,
+ $this->userManager,
+ $this->dispatcher,
+ $this->sharingBackend,
+ $this->config,
+ );
+ // start every test with a empty cards_properties and cards table
+ $query = $this->db->getQueryBuilder();
+ $query->delete('cards_properties')->executeStatement();
+ $query = $this->db->getQueryBuilder();
+ $query->delete('cards')->executeStatement();
+
+ $this->principal->method('getGroupMembership')
+ ->withAnyParameters()
+ ->willReturn([self::UNIT_TEST_GROUP]);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ foreach ($books as $book) {
+ $this->backend->deleteAddressBook($book['id']);
+ }
+ }
+
+ protected function tearDown(): void {
+ if (is_null($this->backend)) {
+ return;
+ }
+
+ $this->principal->method('getGroupMembership')
+ ->withAnyParameters()
+ ->willReturn([self::UNIT_TEST_GROUP]);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ foreach ($books as $book) {
+ $this->backend->deleteAddressBook($book['id']);
+ }
+
+ parent::tearDown();
+ }
+
+ public function testAddressBookOperations(): void {
+ // create a new address book
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+
+ $this->assertEquals(1, $this->backend->getAddressBooksForUserCount(self::UNIT_TEST_USER));
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $this->assertEquals('Example', $books[0]['{DAV:}displayname']);
+ $this->assertEquals('User\'s displayname', $books[0]['{http://nextcloud.com/ns}owner-displayname']);
+
+ // update its display name
+ $patch = new PropPatch([
+ '{DAV:}displayname' => 'Unit test',
+ '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Addressbook used for unit testing'
+ ]);
+ $this->backend->updateAddressBook($books[0]['id'], $patch);
+ $patch->commit();
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $this->assertEquals('Unit test', $books[0]['{DAV:}displayname']);
+ $this->assertEquals('Addressbook used for unit testing', $books[0]['{urn:ietf:params:xml:ns:carddav}addressbook-description']);
+
+ // delete the address book
+ $this->backend->deleteAddressBook($books[0]['id']);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(0, count($books));
+ }
+
+ public function testAddressBookSharing(): void {
+ $this->userManager->expects($this->any())
+ ->method('userExists')
+ ->willReturn(true);
+ $this->groupManager->expects($this->any())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->principal->expects(self::atLeastOnce())
+ ->method('findByUri')
+ ->willReturnOnConsecutiveCalls(self::UNIT_TEST_USER1, self::UNIT_TEST_GROUP);
+
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $l = $this->createMock(IL10N::class);
+ $addressBook = new AddressBook($this->backend, $books[0], $l);
+ $this->backend->updateShares($addressBook, [
+ [
+ 'href' => 'principal:' . self::UNIT_TEST_USER1,
+ ],
+ [
+ 'href' => 'principal:' . self::UNIT_TEST_GROUP,
+ ]
+ ], []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER1);
+ $this->assertEquals(1, count($books));
+
+ // delete the address book
+ $this->backend->deleteAddressBook($books[0]['id']);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(0, count($books));
+ }
+
+ public function testCardOperations(): void {
+ /** @var CardDavBackend&MockObject $backend */
+ $backend = $this->getMockBuilder(CardDavBackend::class)
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config])
+ ->onlyMethods(['updateProperties', 'purgeProperties'])
+ ->getMock();
+
+ // create a new address book
+ $backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $bookId = $books[0]['id'];
+
+ $uri = $this->getUniqueID('card');
+ // updateProperties is expected twice, once for createCard and once for updateCard
+ $calls = [
+ [$bookId, $uri, $this->vcardTest0],
+ [$bookId, $uri, $this->vcardTest1],
+ ];
+ $backend->expects($this->exactly(count($calls)))
+ ->method('updateProperties')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ // Expect event
+ $this->dispatcher
+ ->expects($this->exactly(3))
+ ->method('dispatchTyped');
+
+ // create a card
+ $backend->createCard($bookId, $uri, $this->vcardTest0);
+
+ // get all the cards
+ $cards = $backend->getCards($bookId);
+ $this->assertEquals(1, count($cards));
+ $this->assertEquals($this->vcardTest0, $cards[0]['carddata']);
+
+ // get the cards
+ $card = $backend->getCard($bookId, $uri);
+ $this->assertNotNull($card);
+ $this->assertArrayHasKey('id', $card);
+ $this->assertArrayHasKey('uri', $card);
+ $this->assertArrayHasKey('lastmodified', $card);
+ $this->assertArrayHasKey('etag', $card);
+ $this->assertArrayHasKey('size', $card);
+ $this->assertEquals($this->vcardTest0, $card['carddata']);
+
+ // update the card
+ $backend->updateCard($bookId, $uri, $this->vcardTest1);
+ $card = $backend->getCard($bookId, $uri);
+ $this->assertEquals($this->vcardTest1, $card['carddata']);
+
+ // delete the card
+ $backend->expects($this->once())->method('purgeProperties')->with($bookId, $card['id']);
+ $backend->deleteCard($bookId, $uri);
+ $cards = $backend->getCards($bookId);
+ $this->assertEquals(0, count($cards));
+ }
+
+ public function testMultiCard(): void {
+ $this->backend = $this->getMockBuilder(CardDavBackend::class)
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config])
+ ->onlyMethods(['updateProperties'])
+ ->getMock();
+
+ // create a new address book
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $bookId = $books[0]['id'];
+
+ // create a card
+ $uri0 = self::getUniqueID('card');
+ $this->backend->createCard($bookId, $uri0, $this->vcardTest0);
+ $uri1 = self::getUniqueID('card');
+ $this->backend->createCard($bookId, $uri1, $this->vcardTest1);
+ $uri2 = self::getUniqueID('card');
+ $this->backend->createCard($bookId, $uri2, $this->vcardTest2);
+
+ // get all the cards
+ $cards = $this->backend->getCards($bookId);
+ $this->assertEquals(3, count($cards));
+ usort($cards, function ($a, $b) {
+ return $a['id'] < $b['id'] ? -1 : 1;
+ });
+
+ $this->assertEquals($this->vcardTest0, $cards[0]['carddata']);
+ $this->assertEquals($this->vcardTest1, $cards[1]['carddata']);
+ $this->assertEquals($this->vcardTest2, $cards[2]['carddata']);
+
+ // get the cards 1 & 2 (not 0)
+ $cards = $this->backend->getMultipleCards($bookId, [$uri1, $uri2]);
+ $this->assertEquals(2, count($cards));
+ usort($cards, function ($a, $b) {
+ return $a['id'] < $b['id'] ? -1 : 1;
+ });
+ foreach ($cards as $index => $card) {
+ $this->assertArrayHasKey('id', $card);
+ $this->assertArrayHasKey('uri', $card);
+ $this->assertArrayHasKey('lastmodified', $card);
+ $this->assertArrayHasKey('etag', $card);
+ $this->assertArrayHasKey('size', $card);
+ $this->assertEquals($this->{ 'vcardTest' . ($index + 1) }, $card['carddata']);
+ }
+
+ // delete the card
+ $this->backend->deleteCard($bookId, $uri0);
+ $this->backend->deleteCard($bookId, $uri1);
+ $this->backend->deleteCard($bookId, $uri2);
+ $cards = $this->backend->getCards($bookId);
+ $this->assertEquals(0, count($cards));
+ }
+
+ public function testMultipleUIDOnDifferentAddressbooks(): void {
+ $this->backend = $this->getMockBuilder(CardDavBackend::class)
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config])
+ ->onlyMethods(['updateProperties'])
+ ->getMock();
+
+ // create 2 new address books
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example2', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(2, count($books));
+ $bookId0 = $books[0]['id'];
+ $bookId1 = $books[1]['id'];
+
+ // create a card
+ $uri0 = $this->getUniqueID('card');
+ $this->backend->createCard($bookId0, $uri0, $this->vcardTest0);
+
+ // create another card with same uid but in second address book
+ $uri1 = $this->getUniqueID('card');
+ $this->backend->createCard($bookId1, $uri1, $this->vcardTest0);
+ }
+
+ public function testMultipleUIDDenied(): void {
+ $this->backend = $this->getMockBuilder(CardDavBackend::class)
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
+ ->onlyMethods(['updateProperties'])
+ ->getMock();
+
+ // create a new address book
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $bookId = $books[0]['id'];
+
+ // create a card
+ $uri0 = $this->getUniqueID('card');
+ $this->backend->createCard($bookId, $uri0, $this->vcardTest0);
+
+ // create another card with same uid
+ $uri1 = $this->getUniqueID('card');
+ $this->expectException(BadRequest::class);
+ $test = $this->backend->createCard($bookId, $uri1, $this->vcardTest0);
+ }
+
+ public function testNoValidUID(): void {
+ $this->backend = $this->getMockBuilder(CardDavBackend::class)
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
+ ->onlyMethods(['updateProperties'])
+ ->getMock();
+
+ // create a new address book
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $bookId = $books[0]['id'];
+
+ // create a card without uid
+ $uri1 = $this->getUniqueID('card');
+ $this->expectException(BadRequest::class);
+ $test = $this->backend->createCard($bookId, $uri1, $this->vcardTestNoUID);
+ }
+
+ public function testDeleteWithoutCard(): void {
+ $this->backend = $this->getMockBuilder(CardDavBackend::class)
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
+ ->onlyMethods([
+ 'getCardId',
+ 'addChange',
+ 'purgeProperties',
+ 'updateProperties',
+ ])
+ ->getMock();
+
+ // create a new address book
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getUsersOwnAddressBooks(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+
+ $bookId = $books[0]['id'];
+ $uri = $this->getUniqueID('card');
+
+ // create a new address book
+ $this->backend->expects($this->once())
+ ->method('getCardId')
+ ->with($bookId, $uri)
+ ->willThrowException(new \InvalidArgumentException());
+
+ $calls = [
+ [$bookId, $uri, 1],
+ [$bookId, $uri, 3],
+ ];
+ $this->backend->expects($this->exactly(count($calls)))
+ ->method('addChange')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+ $this->backend->expects($this->never())
+ ->method('purgeProperties');
+
+ // create a card
+ $this->backend->createCard($bookId, $uri, $this->vcardTest0);
+
+ // delete the card
+ $this->assertTrue($this->backend->deleteCard($bookId, $uri));
+ }
+
+ public function testSyncSupport(): void {
+ $this->backend = $this->getMockBuilder(CardDavBackend::class)
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
+ ->onlyMethods(['updateProperties'])
+ ->getMock();
+
+ // create a new address book
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $bookId = $books[0]['id'];
+
+ // fist call without synctoken
+ $changes = $this->backend->getChangesForAddressBook($bookId, '', 1);
+ $syncToken = $changes['syncToken'];
+
+ // add a change
+ $uri0 = $this->getUniqueID('card');
+ $this->backend->createCard($bookId, $uri0, $this->vcardTest0);
+
+ // look for changes
+ $changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1);
+ $this->assertEquals($uri0, $changes['added'][0]);
+ }
+
+ public function testSharing(): void {
+ $this->userManager->expects($this->any())
+ ->method('userExists')
+ ->willReturn(true);
+ $this->groupManager->expects($this->any())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->principal->expects(self::any())
+ ->method('findByUri')
+ ->willReturn(self::UNIT_TEST_USER1);
+
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+
+ $l = $this->createMock(IL10N::class);
+ $exampleBook = new AddressBook($this->backend, $books[0], $l);
+ $this->backend->updateShares($exampleBook, [['href' => 'principal:' . self::UNIT_TEST_USER1]], []);
+
+ $shares = $this->backend->getShares($exampleBook->getResourceId());
+ $this->assertEquals(1, count($shares));
+
+ // adding the same sharee again has no effect
+ $this->backend->updateShares($exampleBook, [['href' => 'principal:' . self::UNIT_TEST_USER1]], []);
+
+ $shares = $this->backend->getShares($exampleBook->getResourceId());
+ $this->assertEquals(1, count($shares));
+
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER1);
+ $this->assertEquals(1, count($books));
+
+ $this->backend->updateShares($exampleBook, [], ['principal:' . self::UNIT_TEST_USER1]);
+
+ $shares = $this->backend->getShares($exampleBook->getResourceId());
+ $this->assertEquals(0, count($shares));
+
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER1);
+ $this->assertEquals(0, count($books));
+ }
+
+ public function testUpdateProperties(): void {
+ $bookId = 42;
+ $cardUri = 'card-uri';
+ $cardId = 2;
+
+ $backend = $this->getMockBuilder(CardDavBackend::class)
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
+ ->onlyMethods(['getCardId'])->getMock();
+
+ $backend->expects($this->any())->method('getCardId')->willReturn($cardId);
+
+ // add properties for new vCard
+ $vCard = new VCard();
+ $vCard->UID = $cardUri;
+ $vCard->FN = 'John Doe';
+ $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]);
+
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from('cards_properties')
+ ->orderBy('name');
+
+ $qResult = $query->execute();
+ $result = $qResult->fetchAll();
+ $qResult->closeCursor();
+
+ $this->assertSame(2, count($result));
+
+ $this->assertSame('FN', $result[0]['name']);
+ $this->assertSame('John Doe', $result[0]['value']);
+ $this->assertSame($bookId, (int)$result[0]['addressbookid']);
+ $this->assertSame($cardId, (int)$result[0]['cardid']);
+
+ $this->assertSame('UID', $result[1]['name']);
+ $this->assertSame($cardUri, $result[1]['value']);
+ $this->assertSame($bookId, (int)$result[1]['addressbookid']);
+ $this->assertSame($cardId, (int)$result[1]['cardid']);
+
+ // update properties for existing vCard
+ $vCard = new VCard();
+ $vCard->UID = $cardUri;
+ $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]);
+
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from('cards_properties');
+
+ $qResult = $query->execute();
+ $result = $qResult->fetchAll();
+ $qResult->closeCursor();
+
+ $this->assertSame(1, count($result));
+
+ $this->assertSame('UID', $result[0]['name']);
+ $this->assertSame($cardUri, $result[0]['value']);
+ $this->assertSame($bookId, (int)$result[0]['addressbookid']);
+ $this->assertSame($cardId, (int)$result[0]['cardid']);
+ }
+
+ public function testPurgeProperties(): void {
+ $query = $this->db->getQueryBuilder();
+ $query->insert('cards_properties')
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(1),
+ 'cardid' => $query->createNamedParameter(1),
+ 'name' => $query->createNamedParameter('name1'),
+ 'value' => $query->createNamedParameter('value1'),
+ 'preferred' => $query->createNamedParameter(0)
+ ]
+ );
+ $query->execute();
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('cards_properties')
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(1),
+ 'cardid' => $query->createNamedParameter(2),
+ 'name' => $query->createNamedParameter('name2'),
+ 'value' => $query->createNamedParameter('value2'),
+ 'preferred' => $query->createNamedParameter(0)
+ ]
+ );
+ $query->execute();
+
+ $this->invokePrivate($this->backend, 'purgeProperties', [1, 1]);
+
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from('cards_properties');
+
+ $qResult = $query->execute();
+ $result = $qResult->fetchAll();
+ $qResult->closeCursor();
+
+ $this->assertSame(1, count($result));
+ $this->assertSame(1, (int)$result[0]['addressbookid']);
+ $this->assertSame(2, (int)$result[0]['cardid']);
+ }
+
+ public function testGetCardId(): void {
+ $query = $this->db->getQueryBuilder();
+
+ $query->insert('cards')
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(1),
+ 'carddata' => $query->createNamedParameter(''),
+ 'uri' => $query->createNamedParameter('uri'),
+ 'lastmodified' => $query->createNamedParameter(4738743),
+ 'etag' => $query->createNamedParameter('etag'),
+ 'size' => $query->createNamedParameter(120)
+ ]
+ );
+ $query->execute();
+ $id = $query->getLastInsertId();
+
+ $this->assertSame($id,
+ $this->invokePrivate($this->backend, 'getCardId', [1, 'uri']));
+ }
+
+
+ public function testGetCardIdFailed(): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $this->invokePrivate($this->backend, 'getCardId', [1, 'uri']);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestSearch')]
+ public function testSearch(string $pattern, array $properties, array $options, array $expected): void {
+ /** @var VCard $vCards */
+ $vCards = [];
+ $vCards[0] = new VCard();
+ $vCards[0]->add(new Text($vCards[0], 'UID', 'uid'));
+ $vCards[0]->add(new Text($vCards[0], 'FN', 'John Doe'));
+ $vCards[0]->add(new Text($vCards[0], 'CLOUD', 'john@nextcloud.com'));
+ $vCards[1] = new VCard();
+ $vCards[1]->add(new Text($vCards[1], 'UID', 'uid'));
+ $vCards[1]->add(new Text($vCards[1], 'FN', 'John M. Doe'));
+ $vCards[2] = new VCard();
+ $vCards[2]->add(new Text($vCards[2], 'UID', 'uid'));
+ $vCards[2]->add(new Text($vCards[2], 'FN', 'find without options'));
+ $vCards[2]->add(new Text($vCards[2], 'CLOUD', 'peter_pan@nextcloud.com'));
+
+ $vCardIds = [];
+ $query = $this->db->getQueryBuilder();
+ for ($i = 0; $i < 3; $i++) {
+ $query->insert($this->dbCardsTable)
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(0),
+ 'carddata' => $query->createNamedParameter($vCards[$i]->serialize(), IQueryBuilder::PARAM_LOB),
+ 'uri' => $query->createNamedParameter('uri' . $i),
+ 'lastmodified' => $query->createNamedParameter(time()),
+ 'etag' => $query->createNamedParameter('etag' . $i),
+ 'size' => $query->createNamedParameter(120),
+ ]
+ );
+ $query->execute();
+ $vCardIds[] = $query->getLastInsertId();
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert($this->dbCardsPropertiesTable)
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(0),
+ 'cardid' => $query->createNamedParameter($vCardIds[0]),
+ 'name' => $query->createNamedParameter('FN'),
+ 'value' => $query->createNamedParameter('John Doe'),
+ 'preferred' => $query->createNamedParameter(0)
+ ]
+ );
+ $query->execute();
+ $query = $this->db->getQueryBuilder();
+ $query->insert($this->dbCardsPropertiesTable)
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(0),
+ 'cardid' => $query->createNamedParameter($vCardIds[0]),
+ 'name' => $query->createNamedParameter('CLOUD'),
+ 'value' => $query->createNamedParameter('John@nextcloud.com'),
+ 'preferred' => $query->createNamedParameter(0)
+ ]
+ );
+ $query->execute();
+ $query = $this->db->getQueryBuilder();
+ $query->insert($this->dbCardsPropertiesTable)
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(0),
+ 'cardid' => $query->createNamedParameter($vCardIds[1]),
+ 'name' => $query->createNamedParameter('FN'),
+ 'value' => $query->createNamedParameter('John M. Doe'),
+ 'preferred' => $query->createNamedParameter(0)
+ ]
+ );
+ $query->execute();
+ $query = $this->db->getQueryBuilder();
+ $query->insert($this->dbCardsPropertiesTable)
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(0),
+ 'cardid' => $query->createNamedParameter($vCardIds[2]),
+ 'name' => $query->createNamedParameter('FN'),
+ 'value' => $query->createNamedParameter('find without options'),
+ 'preferred' => $query->createNamedParameter(0)
+ ]
+ );
+ $query->execute();
+ $query = $this->db->getQueryBuilder();
+ $query->insert($this->dbCardsPropertiesTable)
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(0),
+ 'cardid' => $query->createNamedParameter($vCardIds[2]),
+ 'name' => $query->createNamedParameter('CLOUD'),
+ 'value' => $query->createNamedParameter('peter_pan@nextcloud.com'),
+ 'preferred' => $query->createNamedParameter(0)
+ ]
+ );
+ $query->execute();
+
+ $result = $this->backend->search(0, $pattern, $properties, $options);
+
+ // check result
+ $this->assertSame(count($expected), count($result));
+ $found = [];
+ foreach ($result as $r) {
+ foreach ($expected as $exp) {
+ if ($r['uri'] === $exp[0] && strpos($r['carddata'], $exp[1]) > 0) {
+ $found[$exp[1]] = true;
+ break;
+ }
+ }
+ }
+
+ $this->assertSame(count($expected), count($found));
+ }
+
+ public static function dataTestSearch(): array {
+ return [
+ ['John', ['FN'], [], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]],
+ ['M. Doe', ['FN'], [], [['uri1', 'John M. Doe']]],
+ ['Do', ['FN'], [], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]],
+ 'check if duplicates are handled correctly' => ['John', ['FN', 'CLOUD'], [], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]],
+ 'case insensitive' => ['john', ['FN'], [], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]],
+ 'limit' => ['john', ['FN'], ['limit' => 1], [['uri0', 'John Doe']]],
+ 'limit and offset' => ['john', ['FN'], ['limit' => 1, 'offset' => 1], [['uri1', 'John M. Doe']]],
+ 'find "_" escaped' => ['_', ['CLOUD'], [], [['uri2', 'find without options']]],
+ 'find not empty CLOUD' => ['%_%', ['CLOUD'], ['escape_like_param' => false], [['uri0', 'John Doe'], ['uri2', 'find without options']]],
+ ];
+ }
+
+ public function testGetCardUri(): void {
+ $query = $this->db->getQueryBuilder();
+ $query->insert($this->dbCardsTable)
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(1),
+ 'carddata' => $query->createNamedParameter('carddata', IQueryBuilder::PARAM_LOB),
+ 'uri' => $query->createNamedParameter('uri'),
+ 'lastmodified' => $query->createNamedParameter(5489543),
+ 'etag' => $query->createNamedParameter('etag'),
+ 'size' => $query->createNamedParameter(120),
+ ]
+ );
+ $query->execute();
+
+ $id = $query->getLastInsertId();
+
+ $this->assertSame('uri', $this->backend->getCardUri($id));
+ }
+
+
+ public function testGetCardUriFailed(): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $this->backend->getCardUri(1);
+ }
+
+ public function testGetContact(): void {
+ $query = $this->db->getQueryBuilder();
+ for ($i = 0; $i < 2; $i++) {
+ $query->insert($this->dbCardsTable)
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter($i),
+ 'carddata' => $query->createNamedParameter('carddata' . $i, IQueryBuilder::PARAM_LOB),
+ 'uri' => $query->createNamedParameter('uri' . $i),
+ 'lastmodified' => $query->createNamedParameter(5489543),
+ 'etag' => $query->createNamedParameter('etag' . $i),
+ 'size' => $query->createNamedParameter(120),
+ ]
+ );
+ $query->execute();
+ }
+
+ $result = $this->backend->getContact(0, 'uri0');
+ $this->assertSame(8, count($result));
+ $this->assertSame(0, (int)$result['addressbookid']);
+ $this->assertSame('uri0', $result['uri']);
+ $this->assertSame(5489543, (int)$result['lastmodified']);
+ $this->assertSame('"etag0"', $result['etag']);
+ $this->assertSame(120, (int)$result['size']);
+
+ // this shouldn't return any result because 'uri1' is in address book 1
+ // see https://github.com/nextcloud/server/issues/229
+ $result = $this->backend->getContact(0, 'uri1');
+ $this->assertEmpty($result);
+ }
+
+ public function testGetContactFail(): void {
+ $this->assertEmpty($this->backend->getContact(0, 'uri'));
+ }
+
+ public function testCollectCardProperties(): void {
+ $query = $this->db->getQueryBuilder();
+ $query->insert($this->dbCardsPropertiesTable)
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(666),
+ 'cardid' => $query->createNamedParameter(777),
+ 'name' => $query->createNamedParameter('FN'),
+ 'value' => $query->createNamedParameter('John Doe'),
+ 'preferred' => $query->createNamedParameter(0)
+ ]
+ )
+ ->execute();
+
+ $result = $this->backend->collectCardProperties(666, 'FN');
+ $this->assertEquals(['John Doe'], $result);
+ }
+
+ /**
+ * @throws \OCP\DB\Exception
+ * @throws \Sabre\DAV\Exception\BadRequest
+ */
+ public function testPruneOutdatedSyncTokens(): void {
+ $addressBookId = $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, '', 1);
+ $syncToken = $changes['syncToken'];
+
+ $uri = $this->getUniqueID('card');
+ $this->backend->createCard($addressBookId, $uri, $this->vcardTest0);
+ $this->backend->updateCard($addressBookId, $uri, $this->vcardTest1);
+
+ // Do not delete anything if week data as old as ts=0
+ $deleted = $this->backend->pruneOutdatedSyncTokens(0, 0);
+ self::assertSame(0, $deleted);
+
+ $deleted = $this->backend->pruneOutdatedSyncTokens(0, time());
+ // At least one from the object creation and one from the object update
+ $this->assertGreaterThanOrEqual(2, $deleted);
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 1);
+ $this->assertEmpty($changes['added']);
+ $this->assertEmpty($changes['modified']);
+ $this->assertEmpty($changes['deleted']);
+
+ // Test that objects remain
+
+ // Currently changes are empty
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100);
+ $this->assertEquals(0, count($changes['added'] + $changes['modified'] + $changes['deleted']));
+
+ // Create card
+ $uri = $this->getUniqueID('card');
+ $this->backend->createCard($addressBookId, $uri, $this->vcardTest0);
+ // We now have one add
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100);
+ $this->assertEquals(1, count($changes['added']));
+ $this->assertEmpty($changes['modified']);
+ $this->assertEmpty($changes['deleted']);
+
+ // Update card
+ $this->backend->updateCard($addressBookId, $uri, $this->vcardTest1);
+ // One add, one modify, but shortened to modify
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100);
+ $this->assertEmpty($changes['added']);
+ $this->assertEquals(1, count($changes['modified']));
+ $this->assertEmpty($changes['deleted']);
+
+ // Delete all but last change
+ $deleted = $this->backend->pruneOutdatedSyncTokens(1, time());
+ $this->assertEquals(1, $deleted); // We had two changes before, now one
+
+ // Only update should remain
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100);
+ $this->assertEmpty($changes['added']);
+ $this->assertEquals(1, count($changes['modified']));
+ $this->assertEmpty($changes['deleted']);
+
+ // Check that no crash occurs when prune is called without current changes
+ $deleted = $this->backend->pruneOutdatedSyncTokens(1, time());
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php b/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php
new file mode 100644
index 00000000000..bdd826f671b
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CardDAV;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\ContactsManager;
+use OCA\DAV\Db\PropertyMapper;
+use OCP\Contacts\IManager;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class ContactsManagerTest extends TestCase {
+ public function test(): void {
+ /** @var IManager&MockObject $cm */
+ $cm = $this->createMock(IManager::class);
+ $cm->expects($this->exactly(2))->method('registerAddressBook');
+ $urlGenerator = $this->createMock(IURLGenerator::class);
+ /** @var CardDavBackend&MockObject $backEnd */
+ $backEnd = $this->createMock(CardDavBackend::class);
+ $backEnd->method('getAddressBooksForUser')->willReturn([
+ ['{DAV:}displayname' => 'Test address book', 'uri' => 'default'],
+ ]);
+ $propertyMapper = $this->createMock(PropertyMapper::class);
+
+ $l = $this->createMock(IL10N::class);
+ $app = new ContactsManager($backEnd, $l, $propertyMapper);
+ $app->setupContactsProvider($cm, 'user01', $urlGenerator);
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/ConverterTest.php b/apps/dav/tests/unit/CardDAV/ConverterTest.php
new file mode 100644
index 00000000000..00519b82766
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/ConverterTest.php
@@ -0,0 +1,221 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CardDAV;
+
+use OCA\DAV\CardDAV\Converter;
+use OCP\Accounts\IAccount;
+use OCP\Accounts\IAccountManager;
+use OCP\Accounts\IAccountProperty;
+use OCP\IImage;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class ConverterTest extends TestCase {
+ private IAccountManager&MockObject $accountManager;
+ private IUserManager&MockObject $userManager;
+ private IURLGenerator&MockObject $urlGenerator;
+ private LoggerInterface&MockObject $logger;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->accountManager = $this->createMock(IAccountManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ }
+
+ /**
+ * @return IAccountProperty&MockObject
+ */
+ protected function getAccountPropertyMock(string $name, ?string $value, string $scope) {
+ $property = $this->createMock(IAccountProperty::class);
+ $property->expects($this->any())
+ ->method('getName')
+ ->willReturn($name);
+ $property->expects($this->any())
+ ->method('getValue')
+ ->willReturn((string)$value);
+ $property->expects($this->any())
+ ->method('getScope')
+ ->willReturn($scope);
+ $property->expects($this->any())
+ ->method('getVerified')
+ ->willReturn(IAccountManager::NOT_VERIFIED);
+ return $property;
+ }
+
+ public function getAccountManager(IUser $user) {
+ $account = $this->createMock(IAccount::class);
+ $account->expects($this->any())
+ ->method('getAllProperties')
+ ->willReturnCallback(function () use ($user) {
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_DISPLAYNAME, $user->getDisplayName(), IAccountManager::SCOPE_FEDERATED);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_ADDRESS, '', IAccountManager::SCOPE_LOCAL);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_WEBSITE, '', IAccountManager::SCOPE_LOCAL);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_EMAIL, $user->getEMailAddress(), IAccountManager::SCOPE_FEDERATED);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_AVATAR, $user->getAvatarImage(-1)->data(), IAccountManager::SCOPE_FEDERATED);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_PHONE, '', IAccountManager::SCOPE_LOCAL);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_TWITTER, '', IAccountManager::SCOPE_LOCAL);
+ });
+
+ $accountManager = $this->createMock(IAccountManager::class);
+
+ $accountManager->expects($this->any())
+ ->method('getAccount')
+ ->willReturn($account);
+
+ return $accountManager;
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesNewUsers')]
+ public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null): void {
+ $user = $this->getUserMock((string)$displayName, $eMailAddress, $cloudId);
+ $accountManager = $this->getAccountManager($user);
+
+ $converter = new Converter($accountManager, $this->userManager, $this->urlGenerator, $this->logger);
+ $vCard = $converter->createCardFromUser($user);
+ if ($expectedVCard !== null) {
+ $this->assertInstanceOf('Sabre\VObject\Component\VCard', $vCard);
+ $cardData = $vCard->jsonSerialize();
+ $this->compareData($expectedVCard, $cardData);
+ } else {
+ $this->assertSame($expectedVCard, $vCard);
+ }
+ }
+
+ public function testManagerProp(): void {
+ $user = $this->getUserMock('user', 'user@domain.tld', 'user@cloud.domain.tld');
+ $user->method('getManagerUids')
+ ->willReturn(['mgr']);
+ $this->userManager->expects(self::once())
+ ->method('getDisplayName')
+ ->with('mgr')
+ ->willReturn('Manager');
+ $accountManager = $this->getAccountManager($user);
+
+ $converter = new Converter($accountManager, $this->userManager, $this->urlGenerator, $this->logger);
+ $vCard = $converter->createCardFromUser($user);
+
+ $this->compareData(
+ [
+ 'cloud' => 'user@cloud.domain.tld',
+ 'email' => 'user@domain.tld',
+ 'x-managersname' => 'Manager',
+ ],
+ $vCard->jsonSerialize()
+ );
+ }
+
+ protected function compareData(array $expected, array $data): void {
+ foreach ($expected as $key => $value) {
+ $found = false;
+ foreach ($data[1] as $d) {
+ if ($d[0] === $key && $d[3] === $value) {
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ $this->assertTrue(false, 'Expected data: ' . $key . ' not found.');
+ }
+ }
+ }
+
+ public static function providesNewUsers(): array {
+ return [
+ [
+ null
+ ],
+ [
+ null,
+ null,
+ 'foo@bar.net'
+ ],
+ [
+ [
+ 'cloud' => 'foo@cloud.net',
+ 'email' => 'foo@bar.net',
+ 'photo' => 'MTIzNDU2Nzg5',
+ ],
+ null,
+ 'foo@bar.net',
+ 'foo@cloud.net'
+ ],
+ [
+ [
+ 'cloud' => 'foo@cloud.net',
+ 'email' => 'foo@bar.net',
+ 'fn' => 'Dr. Foo Bar',
+ 'photo' => 'MTIzNDU2Nzg5',
+ ],
+ 'Dr. Foo Bar',
+ 'foo@bar.net',
+ 'foo@cloud.net'
+ ],
+ [
+ [
+ 'cloud' => 'foo@cloud.net',
+ 'fn' => 'Dr. Foo Bar',
+ 'photo' => 'MTIzNDU2Nzg5',
+ ],
+ 'Dr. Foo Bar',
+ null,
+ 'foo@cloud.net'
+ ],
+ [
+ [
+ 'cloud' => 'foo@cloud.net',
+ 'fn' => 'Dr. Foo Bar',
+ 'photo' => 'MTIzNDU2Nzg5',
+ ],
+ 'Dr. Foo Bar',
+ '',
+ 'foo@cloud.net'
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesNames')]
+ public function testNameSplitter(string $expected, string $fullName): void {
+ $converter = new Converter($this->accountManager, $this->userManager, $this->urlGenerator, $this->logger);
+ $r = $converter->splitFullName($fullName);
+ $r = implode(';', $r);
+ $this->assertEquals($expected, $r);
+ }
+
+ public static function providesNames(): array {
+ return [
+ ['Sauron;;;;', 'Sauron'],
+ ['Baggins;Bilbo;;;', 'Bilbo Baggins'],
+ ['Tolkien;John;Ronald Reuel;;', 'John Ronald Reuel Tolkien'],
+ ];
+ }
+
+ /**
+ * @return IUser&MockObject
+ */
+ protected function getUserMock(string $displayName, ?string $eMailAddress, ?string $cloudId) {
+ $image0 = $this->createMock(IImage::class);
+ $image0->method('mimeType')->willReturn('image/jpeg');
+ $image0->method('data')->willReturn('123456789');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('12345');
+ $user->method('getDisplayName')->willReturn($displayName);
+ $user->method('getEMailAddress')->willReturn($eMailAddress);
+ $user->method('getCloudId')->willReturn($cloudId);
+ $user->method('getAvatarImage')->willReturn($image0);
+ return $user;
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php b/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php
new file mode 100644
index 00000000000..d47f53bddcd
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php
@@ -0,0 +1,174 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CardDAV;
+
+use OCA\DAV\CardDAV\AddressBook;
+use OCA\DAV\CardDAV\ImageExportPlugin;
+use OCA\DAV\CardDAV\PhotoCache;
+use OCP\AppFramework\Http;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\CardDAV\Card;
+use Sabre\DAV\Node;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+class ImageExportPluginTest extends TestCase {
+ private ResponseInterface&MockObject $response;
+ private RequestInterface&MockObject $request;
+ private Server&MockObject $server;
+ private Tree&MockObject $tree;
+ private PhotoCache&MockObject $cache;
+ private ImageExportPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->response = $this->createMock(ResponseInterface::class);
+ $this->server = $this->createMock(Server::class);
+ $this->tree = $this->createMock(Tree::class);
+ $this->server->tree = $this->tree;
+ $this->cache = $this->createMock(PhotoCache::class);
+
+ $this->plugin = new ImageExportPlugin($this->cache);
+ $this->plugin->initialize($this->server);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesQueryParams')]
+ public function testQueryParams(array $param): void {
+ $this->request->expects($this->once())->method('getQueryParameters')->willReturn($param);
+ $result = $this->plugin->httpGet($this->request, $this->response);
+ $this->assertTrue($result);
+ }
+
+ public static function providesQueryParams(): array {
+ return [
+ [[]],
+ [['1']],
+ [['foo' => 'bar']],
+ ];
+ }
+
+ public function testNoCard(): void {
+ $this->request->method('getQueryParameters')
+ ->willReturn([
+ 'photo'
+ ]);
+ $this->request->method('getPath')
+ ->willReturn('user/book/card');
+
+ $node = $this->createMock(Node::class);
+ $this->tree->method('getNodeForPath')
+ ->with('user/book/card')
+ ->willReturn($node);
+
+ $result = $this->plugin->httpGet($this->request, $this->response);
+ $this->assertTrue($result);
+ }
+
+ public static function dataTestCard(): array {
+ return [
+ [null, false],
+ [null, true],
+ [32, false],
+ [32, true],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCard')]
+ public function testCard(?int $size, bool $photo): void {
+ $query = ['photo' => null];
+ if ($size !== null) {
+ $query['size'] = $size;
+ }
+
+ $this->request->method('getQueryParameters')
+ ->willReturn($query);
+ $this->request->method('getPath')
+ ->willReturn('user/book/card');
+
+ $card = $this->createMock(Card::class);
+ $card->method('getETag')
+ ->willReturn('"myEtag"');
+ $card->method('getName')
+ ->willReturn('card');
+ $book = $this->createMock(AddressBook::class);
+ $book->method('getResourceId')
+ ->willReturn(1);
+
+ $this->tree->method('getNodeForPath')
+ ->willReturnCallback(function ($path) use ($card, $book) {
+ if ($path === 'user/book/card') {
+ return $card;
+ } elseif ($path === 'user/book') {
+ return $book;
+ }
+ $this->fail();
+ });
+
+ $size = $size === null ? -1 : $size;
+
+ if ($photo) {
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getMimeType')
+ ->willReturn('image/jpeg');
+ $file->method('getContent')
+ ->willReturn('imgdata');
+
+ $this->cache->method('get')
+ ->with(1, 'card', $size, $card)
+ ->willReturn($file);
+
+ $setHeaderCalls = [
+ ['Cache-Control', 'private, max-age=3600, must-revalidate'],
+ ['Etag', '"myEtag"'],
+ ['Content-Type', 'image/jpeg'],
+ ['Content-Disposition', 'attachment; filename=card.jpg'],
+ ];
+ $this->response->expects($this->exactly(count($setHeaderCalls)))
+ ->method('setHeader')
+ ->willReturnCallback(function () use (&$setHeaderCalls): void {
+ $expected = array_shift($setHeaderCalls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ $this->response->expects($this->once())
+ ->method('setStatus')
+ ->with(200);
+ $this->response->expects($this->once())
+ ->method('setBody')
+ ->with('imgdata');
+ } else {
+ $setHeaderCalls = [
+ ['Cache-Control', 'private, max-age=3600, must-revalidate'],
+ ['Etag', '"myEtag"'],
+ ];
+ $this->response->expects($this->exactly(count($setHeaderCalls)))
+ ->method('setHeader')
+ ->willReturnCallback(function () use (&$setHeaderCalls): void {
+ $expected = array_shift($setHeaderCalls);
+ $this->assertEquals($expected, func_get_args());
+ });
+ $this->cache->method('get')
+ ->with(1, 'card', $size, $card)
+ ->willThrowException(new NotFoundException());
+ $this->response->expects($this->once())
+ ->method('setStatus')
+ ->with(Http::STATUS_NO_CONTENT);
+ }
+
+ $result = $this->plugin->httpGet($this->request, $this->response);
+ $this->assertFalse($result);
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php b/apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php
new file mode 100644
index 00000000000..ee599d5a76c
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php
@@ -0,0 +1,146 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CardDAV\Security;
+
+use OC\Security\RateLimiting\Exception\RateLimitExceededException;
+use OC\Security\RateLimiting\Limiter;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin;
+use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
+use OCP\IAppConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\Forbidden;
+use Test\TestCase;
+
+class CardDavRateLimitingPluginTest extends TestCase {
+
+ private Limiter&MockObject $limiter;
+ private CardDavBackend&MockObject $cardDavBackend;
+ private IUserManager&MockObject $userManager;
+ private LoggerInterface&MockObject $logger;
+ private IAppConfig&MockObject $config;
+ private string $userId = 'user123';
+ private CardDavRateLimitingPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->limiter = $this->createMock(Limiter::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->cardDavBackend = $this->createMock(CardDavBackend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->config = $this->createMock(IAppConfig::class);
+ $this->plugin = new CardDavRateLimitingPlugin(
+ $this->limiter,
+ $this->userManager,
+ $this->cardDavBackend,
+ $this->logger,
+ $this->config,
+ $this->userId,
+ );
+ }
+
+ public function testNoUserObject(): void {
+ $this->limiter->expects(self::never())
+ ->method('registerUserRequest');
+
+ $this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
+ }
+
+ public function testUnrelated(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->limiter->expects(self::never())
+ ->method('registerUserRequest');
+
+ $this->plugin->beforeBind('foo/bar');
+ }
+
+ public function testRegisterAddressBookrCreation(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'carddav-create-address-book',
+ 10,
+ 3600,
+ $user,
+ );
+
+ $this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
+ }
+
+ public function testAddressBookCreationRateLimitExceeded(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'carddav-create-address-book',
+ 10,
+ 3600,
+ $user,
+ )
+ ->willThrowException(new RateLimitExceededException());
+ $this->expectException(TooManyRequests::class);
+
+ $this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
+ }
+
+ public function testAddressBookLimitReached(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $user->method('getUID')->willReturn('user123');
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'carddav-create-address-book',
+ 10,
+ 3600,
+ $user,
+ );
+ $this->cardDavBackend->expects(self::once())
+ ->method('getAddressBooksForUserCount')
+ ->with('principals/users/user123')
+ ->willReturn(11);
+ $this->expectException(Forbidden::class);
+
+ $this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
+ }
+
+}
diff --git a/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php b/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php
new file mode 100644
index 00000000000..1e934a69a53
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CardDAV\Sharing;
+
+use OCA\DAV\Connector\Sabre\Auth;
+use OCA\DAV\DAV\Sharing\IShareable;
+use OCA\DAV\DAV\Sharing\Plugin;
+use OCP\IConfig;
+use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Server;
+use Sabre\DAV\SimpleCollection;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+use Test\TestCase;
+
+class PluginTest extends TestCase {
+ private Plugin $plugin;
+ private Server $server;
+ private IShareable&MockObject $book;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $authBackend = $this->createMock(Auth::class);
+ $authBackend->method('isDavAuthenticated')
+ ->willReturn(true);
+ $request = $this->createMock(IRequest::class);
+ $config = $this->createMock(IConfig::class);
+ $this->plugin = new Plugin($authBackend, $request, $config);
+
+ $root = new SimpleCollection('root');
+ $this->server = new \Sabre\DAV\Server($root);
+ $this->book = $this->createMock(IShareable::class);
+ $this->book->method('getName')
+ ->willReturn('addressbook1.vcf');
+ $root->addChild($this->book);
+ $this->plugin->initialize($this->server);
+ }
+
+ public function testSharing(): void {
+ $this->book->expects($this->once())->method('updateShares')->with([[
+ 'href' => 'principal:principals/admin',
+ 'commonName' => null,
+ 'summary' => null,
+ 'readOnly' => false
+ ]], ['mailto:wilfredo@example.com']);
+
+ // setup request
+ $request = new Request('POST', 'addressbook1.vcf');
+ $request->addHeader('Content-Type', 'application/xml');
+ $request->setBody('<?xml version="1.0" encoding="utf-8" ?><CS:share xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns"><CS:set><D:href>principal:principals/admin</D:href><CS:read-write/></CS:set> <CS:remove><D:href>mailto:wilfredo@example.com</D:href></CS:remove></CS:share>');
+ $response = new Response();
+ $this->plugin->httpPost($request, $response);
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php
new file mode 100644
index 00000000000..77caed336f4
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php
@@ -0,0 +1,480 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\CardDAV;
+
+use GuzzleHttp\Exception\ClientException;
+use GuzzleHttp\Psr7\Request as PsrRequest;
+use GuzzleHttp\Psr7\Response as PsrResponse;
+use OC\Http\Client\Response;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\Converter;
+use OCA\DAV\CardDAV\SyncService;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Http\Client\ClientExceptionInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Sabre\VObject\Component\VCard;
+use Test\TestCase;
+
+class SyncServiceTest extends TestCase {
+
+ protected CardDavBackend&MockObject $backend;
+ protected IUserManager&MockObject $userManager;
+ protected IDBConnection&MockObject $dbConnection;
+ protected LoggerInterface $logger;
+ protected Converter&MockObject $converter;
+ protected IClient&MockObject $client;
+ protected IConfig&MockObject $config;
+ protected SyncService $service;
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $addressBook = [
+ 'id' => 1,
+ 'uri' => 'system',
+ 'principaluri' => 'principals/system/system',
+ '{DAV:}displayname' => 'system',
+ // watch out, incomplete address book mock.
+ ];
+
+ $this->backend = $this->createMock(CardDavBackend::class);
+ $this->backend->method('getAddressBooksByUri')
+ ->with('principals/system/system', 1)
+ ->willReturn($addressBook);
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->dbConnection = $this->createMock(IDBConnection::class);
+ $this->logger = new NullLogger();
+ $this->converter = $this->createMock(Converter::class);
+ $this->client = $this->createMock(IClient::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $clientService = $this->createMock(IClientService::class);
+ $clientService->method('newClient')
+ ->willReturn($this->client);
+
+ $this->service = new SyncService(
+ $this->backend,
+ $this->userManager,
+ $this->dbConnection,
+ $this->logger,
+ $this->converter,
+ $clientService,
+ $this->config
+ );
+ }
+
+ public function testEmptySync(): void {
+ $this->backend->expects($this->exactly(0))
+ ->method('createCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('updateCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('deleteCard');
+
+ $body = '<?xml version="1.0"?>
+<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
+ <d:sync-token>http://sabre.io/ns/sync/1</d:sync-token>
+</d:multistatus>';
+
+ $requestResponse = new Response(new PsrResponse(
+ 207,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ ));
+
+ $this->client
+ ->method('request')
+ ->willReturn($requestResponse);
+
+ $token = $this->service->syncRemoteAddressBook(
+ '',
+ 'system',
+ 'system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ )[0];
+
+ $this->assertEquals('http://sabre.io/ns/sync/1', $token);
+ }
+
+ public function testSyncWithNewElement(): void {
+ $this->backend->expects($this->exactly(1))
+ ->method('createCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('updateCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('deleteCard');
+
+ $this->backend->method('getCard')
+ ->willReturn(false);
+
+
+ $body = '<?xml version="1.0"?>
+<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
+ <d:response>
+ <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getcontenttype>text/vcard; charset=utf-8</d:getcontenttype>
+ <d:getetag>&quot;2df155fa5c2a24cd7f750353fc63f037&quot;</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:sync-token>http://sabre.io/ns/sync/2</d:sync-token>
+</d:multistatus>';
+
+ $reportResponse = new Response(new PsrResponse(
+ 207,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ ));
+
+ $this->client
+ ->method('request')
+ ->willReturn($reportResponse);
+
+ $vCard = 'BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.5.4//EN
+UID:alice
+FN;X-NC-SCOPE=v2-federated:alice
+N;X-NC-SCOPE=v2-federated:alice;;;;
+X-SOCIALPROFILE;TYPE=NEXTCLOUD;X-NC-SCOPE=v2-published:https://server2.internal/index.php/u/alice
+CLOUD:alice@server2.internal
+END:VCARD';
+
+ $getResponse = new Response(new PsrResponse(
+ 200,
+ ['Content-Type' => 'text/vcard; charset=utf-8', 'Content-Length' => strlen($vCard)],
+ $vCard,
+ ));
+
+ $this->client
+ ->method('get')
+ ->willReturn($getResponse);
+
+ $token = $this->service->syncRemoteAddressBook(
+ '',
+ 'system',
+ 'system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ )[0];
+
+ $this->assertEquals('http://sabre.io/ns/sync/2', $token);
+ }
+
+ public function testSyncWithUpdatedElement(): void {
+ $this->backend->expects($this->exactly(0))
+ ->method('createCard');
+ $this->backend->expects($this->exactly(1))
+ ->method('updateCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('deleteCard');
+
+ $this->backend->method('getCard')
+ ->willReturn(true);
+
+
+ $body = '<?xml version="1.0"?>
+<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
+ <d:response>
+ <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getcontenttype>text/vcard; charset=utf-8</d:getcontenttype>
+ <d:getetag>&quot;2df155fa5c2a24cd7f750353fc63f037&quot;</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:sync-token>http://sabre.io/ns/sync/3</d:sync-token>
+</d:multistatus>';
+
+ $reportResponse = new Response(new PsrResponse(
+ 207,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ ));
+
+ $this->client
+ ->method('request')
+ ->willReturn($reportResponse);
+
+ $vCard = 'BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.5.4//EN
+UID:alice
+FN;X-NC-SCOPE=v2-federated:alice
+N;X-NC-SCOPE=v2-federated:alice;;;;
+X-SOCIALPROFILE;TYPE=NEXTCLOUD;X-NC-SCOPE=v2-published:https://server2.internal/index.php/u/alice
+CLOUD:alice@server2.internal
+END:VCARD';
+
+ $getResponse = new Response(new PsrResponse(
+ 200,
+ ['Content-Type' => 'text/vcard; charset=utf-8', 'Content-Length' => strlen($vCard)],
+ $vCard,
+ ));
+
+ $this->client
+ ->method('get')
+ ->willReturn($getResponse);
+
+ $token = $this->service->syncRemoteAddressBook(
+ '',
+ 'system',
+ 'system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ )[0];
+
+ $this->assertEquals('http://sabre.io/ns/sync/3', $token);
+ }
+
+ public function testSyncWithDeletedElement(): void {
+ $this->backend->expects($this->exactly(0))
+ ->method('createCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('updateCard');
+ $this->backend->expects($this->exactly(1))
+ ->method('deleteCard');
+
+ $body = '<?xml version="1.0"?>
+<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
+<d:response>
+ <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href>
+ <d:status>HTTP/1.1 404 Not Found</d:status>
+</d:response>
+<d:sync-token>http://sabre.io/ns/sync/4</d:sync-token>
+</d:multistatus>';
+
+ $reportResponse = new Response(new PsrResponse(
+ 207,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ ));
+
+ $this->client
+ ->method('request')
+ ->willReturn($reportResponse);
+
+ $token = $this->service->syncRemoteAddressBook(
+ '',
+ 'system',
+ 'system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ )[0];
+
+ $this->assertEquals('http://sabre.io/ns/sync/4', $token);
+ }
+
+ public function testEnsureSystemAddressBookExists(): void {
+ /** @var CardDavBackend&MockObject $backend */
+ $backend = $this->createMock(CardDavBackend::class);
+ $backend->expects($this->exactly(1))->method('createAddressBook');
+ $backend->expects($this->exactly(2))
+ ->method('getAddressBooksByUri')
+ ->willReturnOnConsecutiveCalls(
+ null,
+ [],
+ );
+
+ $userManager = $this->createMock(IUserManager::class);
+ $dbConnection = $this->createMock(IDBConnection::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $converter = $this->createMock(Converter::class);
+ $clientService = $this->createMock(IClientService::class);
+ $config = $this->createMock(IConfig::class);
+
+ $ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config);
+ $ss->ensureSystemAddressBookExists('principals/users/adam', 'contacts', []);
+ }
+
+ public static function dataActivatedUsers(): array {
+ return [
+ [true, 1, 1, 1],
+ [false, 0, 0, 3],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataActivatedUsers')]
+ public function testUpdateAndDeleteUser(bool $activated, int $createCalls, int $updateCalls, int $deleteCalls): void {
+ /** @var CardDavBackend | MockObject $backend */
+ $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock();
+ $logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock();
+
+ $backend->expects($this->exactly($createCalls))->method('createCard');
+ $backend->expects($this->exactly($updateCalls))->method('updateCard');
+ $backend->expects($this->exactly($deleteCalls))->method('deleteCard');
+
+ $backend->method('getCard')->willReturnOnConsecutiveCalls(false, [
+ 'carddata' => "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.8//EN\r\nUID:test-user\r\nFN:test-user\r\nN:test-user;;;;\r\nEND:VCARD\r\n\r\n"
+ ]);
+
+ $backend->method('getAddressBooksByUri')
+ ->with('principals/system/system', 'system')
+ ->willReturn(['id' => -1]);
+
+ $userManager = $this->createMock(IUserManager::class);
+ $dbConnection = $this->createMock(IDBConnection::class);
+ $user = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('unittest');
+ $user->method('getUID')->willReturn('test-user');
+ $user->method('getCloudId')->willReturn('cloudId');
+ $user->method('getDisplayName')->willReturn('test-user');
+ $user->method('isEnabled')->willReturn($activated);
+ $converter = $this->createMock(Converter::class);
+ $converter->expects($this->any())
+ ->method('createCardFromUser')
+ ->willReturn($this->createMock(VCard::class));
+
+ $clientService = $this->createMock(IClientService::class);
+ $config = $this->createMock(IConfig::class);
+
+ $ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config);
+ $ss->updateUser($user);
+
+ $ss->updateUser($user);
+
+ $ss->deleteUser($user);
+ }
+
+ public function testDeleteAddressbookWhenAccessRevoked(): void {
+ $this->expectException(ClientExceptionInterface::class);
+
+ $this->backend->expects($this->exactly(0))
+ ->method('createCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('updateCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('deleteCard');
+ $this->backend->expects($this->exactly(1))
+ ->method('deleteAddressBook');
+
+ $request = new PsrRequest(
+ 'REPORT',
+ 'https://server2.internal/remote.php/dav/addressbooks/system/system/system',
+ ['Content-Type' => 'application/xml'],
+ );
+
+ $body = '<?xml version="1.0" encoding="utf-8"?>
+<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
+ <s:exception>Sabre\DAV\Exception\NotAuthenticated</s:exception>
+ <s:message>No public access to this resource., Username or password was incorrect, No \'Authorization: Bearer\' header found. Either the client didn\'t send one, or the server is mis-configured, Username or password was incorrect</s:message>
+</d:error>';
+
+ $response = new PsrResponse(
+ 401,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ );
+
+ $message = 'Client error: `REPORT https://server2.internal/cloud/remote.php/dav/addressbooks/system/system/system` resulted in a `401 Unauthorized` response:
+<?xml version="1.0" encoding="utf-8"?>
+<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
+ <s:exception>Sabre\DA (truncated...)
+';
+
+ $reportException = new ClientException(
+ $message,
+ $request,
+ $response
+ );
+
+ $this->client
+ ->method('request')
+ ->willThrowException($reportException);
+
+ $this->service->syncRemoteAddressBook(
+ '',
+ 'system',
+ 'system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providerUseAbsoluteUriReport')]
+ public function testUseAbsoluteUriReport(string $host, string $expected): void {
+ $body = '<?xml version="1.0"?>
+<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
+ <d:sync-token>http://sabre.io/ns/sync/1</d:sync-token>
+</d:multistatus>';
+
+ $requestResponse = new Response(new PsrResponse(
+ 207,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ ));
+
+ $this->client
+ ->method('request')
+ ->with(
+ 'REPORT',
+ $this->callback(function ($uri) use ($expected) {
+ $this->assertEquals($expected, $uri);
+ return true;
+ }),
+ $this->callback(function ($options) {
+ $this->assertIsArray($options);
+ return true;
+ }),
+ )
+ ->willReturn($requestResponse);
+
+ $this->service->syncRemoteAddressBook(
+ $host,
+ 'system',
+ 'remote.php/dav/addressbooks/system/system/system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ );
+ }
+
+ public static function providerUseAbsoluteUriReport(): array {
+ return [
+ ['https://server.internal', 'https://server.internal/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal/', 'https://server.internal/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal/nextcloud', 'https://server.internal/nextcloud/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal/nextcloud/', 'https://server.internal/nextcloud/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal:8080', 'https://server.internal:8080/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal:8080/', 'https://server.internal:8080/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal:8080/nextcloud', 'https://server.internal:8080/nextcloud/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal:8080/nextcloud/', 'https://server.internal:8080/nextcloud/remote.php/dav/addressbooks/system/system/system'],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php b/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php
new file mode 100644
index 00000000000..4a218fa4616
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php
@@ -0,0 +1,428 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CardDAV;
+
+use OC\AppFramework\Http\Request;
+use OCA\DAV\CardDAV\SyncService;
+use OCA\DAV\CardDAV\SystemAddressbook;
+use OCA\Federation\TrustedServers;
+use OCP\Accounts\IAccountManager;
+use OCP\IConfig;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\CardDAV\Backend\BackendInterface;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\VObject\Component\VCard;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+class SystemAddressBookTest extends TestCase {
+ private BackendInterface&MockObject $cardDavBackend;
+ private array $addressBookInfo;
+ private IL10N&MockObject $l10n;
+ private IConfig&MockObject $config;
+ private IUserSession $userSession;
+ private IRequest&MockObject $request;
+ private array $server;
+ private TrustedServers&MockObject $trustedServers;
+ private IGroupManager&MockObject $groupManager;
+ private SystemAddressbook $addressBook;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->cardDavBackend = $this->createMock(BackendInterface::class);
+ $this->addressBookInfo = [
+ 'id' => 123,
+ '{DAV:}displayname' => 'Accounts',
+ 'principaluri' => 'principals/system/system',
+ ];
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->request = $this->createMock(Request::class);
+ $this->server = [
+ 'PHP_AUTH_USER' => 'system',
+ 'PHP_AUTH_PW' => 'shared123',
+ ];
+ $this->request->method('__get')->with('server')->willReturn($this->server);
+ $this->trustedServers = $this->createMock(TrustedServers::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+
+ $this->addressBook = new SystemAddressbook(
+ $this->cardDavBackend,
+ $this->addressBookInfo,
+ $this->l10n,
+ $this->config,
+ $this->userSession,
+ $this->request,
+ $this->trustedServers,
+ $this->groupManager,
+ );
+ }
+
+ public function testGetChildrenAsGuest(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user');
+ $user->method('getBackendClassName')->willReturn('Guests');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $vcfWithScopes = <<<VCF
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.4.2//EN
+UID:admin
+FN;X-NC-SCOPE=v2-federated:admin
+N;X-NC-SCOPE=v2-federated:admin;;;;
+ADR;TYPE=OTHER;X-NC-SCOPE=v2-local:Testing test test test;;;;;;
+EMAIL;TYPE=OTHER;X-NC-SCOPE=v2-federated:miau_lalala@gmx.net
+TEL;TYPE=OTHER;X-NC-SCOPE=v2-local:+435454454544
+CLOUD:admin@http://localhost
+END:VCARD
+VCF;
+ $originalCard = [
+ 'carddata' => $vcfWithScopes,
+ ];
+ $this->cardDavBackend->expects(self::once())
+ ->method('getCard')
+ ->with(123, 'Guests:user.vcf')
+ ->willReturn($originalCard);
+
+ $children = $this->addressBook->getChildren();
+
+ self::assertCount(1, $children);
+ }
+
+ public function testGetFilteredChildForFederation(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $this->trustedServers->expects(self::once())
+ ->method('getServers')
+ ->willReturn([
+ [
+ 'shared_secret' => 'shared123',
+ ],
+ ]);
+ $vcfWithScopes = <<<VCF
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.4.2//EN
+UID:admin
+FN;X-NC-SCOPE=v2-federated:admin
+N;X-NC-SCOPE=v2-federated:admin;;;;
+ADR;TYPE=OTHER;X-NC-SCOPE=v2-local:Testing test test test;;;;;;
+EMAIL;TYPE=OTHER;X-NC-SCOPE=v2-federated:miau_lalala@gmx.net
+TEL;TYPE=OTHER;X-NC-SCOPE=v2-local:+435454454544
+CLOUD:admin@http://localhost
+END:VCARD
+VCF;
+ $originalCard = [
+ 'carddata' => $vcfWithScopes,
+ ];
+ $this->cardDavBackend->expects(self::once())
+ ->method('getCard')
+ ->with(123, 'user.vcf')
+ ->willReturn($originalCard);
+
+ $card = $this->addressBook->getChild('user.vcf');
+
+ /** @var VCard $vCard */
+ $vCard = Reader::read($card->get());
+ foreach ($vCard->children() as $child) {
+ $scope = $child->offsetGet('X-NC-SCOPE');
+ if ($scope !== null) {
+ self::assertNotEquals(IAccountManager::SCOPE_PRIVATE, $scope->getValue());
+ self::assertNotEquals(IAccountManager::SCOPE_LOCAL, $scope->getValue());
+ }
+ }
+ }
+
+ public function testGetChildNotFound(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $this->trustedServers->expects(self::once())
+ ->method('getServers')
+ ->willReturn([
+ [
+ 'shared_secret' => 'shared123',
+ ],
+ ]);
+ $this->expectException(NotFound::class);
+
+ $this->addressBook->getChild('LDAP:user.vcf');
+ }
+
+ public function testGetChildWithoutEnumeration(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $this->expectException(Forbidden::class);
+
+ $this->addressBook->getChild('LDAP:user.vcf');
+ }
+
+ public function testGetChildAsGuest(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('Guests');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->expectException(Forbidden::class);
+
+ $this->addressBook->getChild('LDAP:user.vcf');
+ }
+
+ public function testGetChildWithGroupEnumerationRestriction(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('LDAP');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $otherUser = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('LDAP');
+ $otherUser->method('getUID')->willReturn('other');
+ $group = $this->createMock(IGroup::class);
+ $group->expects(self::once())
+ ->method('getUsers')
+ ->willReturn([$otherUser]);
+ $this->groupManager->expects(self::once())
+ ->method('getUserGroups')
+ ->with($user)
+ ->willReturn([$group]);
+ $cardData = <<<VCF
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.4.2//EN
+UID:admin
+FN;X-NC-SCOPE=v2-federated:other
+END:VCARD
+VCF;
+ $this->cardDavBackend->expects(self::once())
+ ->method('getCard')
+ ->with($this->addressBookInfo['id'], "{$otherUser->getBackendClassName()}:{$otherUser->getUID()}.vcf")
+ ->willReturn([
+ 'id' => 123,
+ 'carddata' => $cardData,
+ ]);
+
+ $this->addressBook->getChild("{$otherUser->getBackendClassName()}:{$otherUser->getUID()}.vcf");
+ }
+
+ public function testGetChildWithPhoneNumberEnumerationRestriction(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('LDAP');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->expectException(Forbidden::class);
+
+ $this->addressBook->getChild('LDAP:user.vcf');
+ }
+
+ public function testGetOwnChildWithPhoneNumberEnumerationRestriction(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('LDAP');
+ $user->method('getUID')->willReturn('user');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $cardData = <<<VCF
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.4.2//EN
+UID:admin
+FN;X-NC-SCOPE=v2-federated:user
+END:VCARD
+VCF;
+ $this->cardDavBackend->expects(self::once())
+ ->method('getCard')
+ ->with($this->addressBookInfo['id'], 'LDAP:user.vcf')
+ ->willReturn([
+ 'id' => 123,
+ 'carddata' => $cardData,
+ ]);
+
+ $this->addressBook->getChild('LDAP:user.vcf');
+ }
+
+ public function testGetMultipleChildrenWithGroupEnumerationRestriction(): void {
+ $this->config
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user');
+ $user->method('getBackendClassName')->willReturn('LDAP');
+ $other1 = $this->createMock(IUser::class);
+ $other1->method('getUID')->willReturn('other1');
+ $other1->method('getBackendClassName')->willReturn('LDAP');
+ $other2 = $this->createMock(IUser::class);
+ $other2->method('getUID')->willReturn('other2');
+ $other2->method('getBackendClassName')->willReturn('LDAP');
+ $other3 = $this->createMock(IUser::class);
+ $other3->method('getUID')->willReturn('other3');
+ $other3->method('getBackendClassName')->willReturn('LDAP');
+ $this->userSession
+ ->method('getUser')
+ ->willReturn($user);
+ $group1 = $this->createMock(IGroup::class);
+ $group1
+ ->method('getUsers')
+ ->willReturn([$user, $other1]);
+ $group2 = $this->createMock(IGroup::class);
+ $group2
+ ->method('getUsers')
+ ->willReturn([$other1, $other2, $user]);
+ $this->groupManager
+ ->method('getUserGroups')
+ ->with($user)
+ ->willReturn([$group1]);
+ $this->cardDavBackend->expects(self::once())
+ ->method('getMultipleCards')
+ ->with($this->addressBookInfo['id'], [
+ SyncService::getCardUri($user),
+ SyncService::getCardUri($other1),
+ ])
+ ->willReturn([
+ [],
+ [],
+ ]);
+
+ $cards = $this->addressBook->getMultipleChildren([
+ SyncService::getCardUri($user),
+ SyncService::getCardUri($other1),
+ // SyncService::getCardUri($other2), // Omitted to test that it's not returned as stray
+ SyncService::getCardUri($other3), // No overlapping group with this one
+ ]);
+
+ self::assertCount(2, $cards);
+ }
+
+ public function testGetMultipleChildrenAsGuest(): void {
+ $this->config
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user');
+ $user->method('getBackendClassName')->willReturn('Guests');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $cards = $this->addressBook->getMultipleChildren(['Database:user1.vcf', 'LDAP:user2.vcf']);
+
+ self::assertEmpty($cards);
+ }
+
+ public function testGetMultipleChildren(): void {
+ $this->config
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $this->trustedServers
+ ->method('getServers')
+ ->willReturn([
+ [
+ 'shared_secret' => 'shared123',
+ ],
+ ]);
+ $cardData = <<<VCF
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.4.2//EN
+UID:admin
+FN;X-NC-SCOPE=v2-federated:user
+END:VCARD
+VCF;
+ $this->cardDavBackend->expects(self::once())
+ ->method('getMultipleCards')
+ ->with($this->addressBookInfo['id'], ['Database:user1.vcf', 'LDAP:user2.vcf'])
+ ->willReturn([
+ [
+ 'id' => 123,
+ 'carddata' => $cardData,
+ ],
+ [
+ 'id' => 321,
+ 'carddata' => $cardData,
+ ],
+ ]);
+
+ $cards = $this->addressBook->getMultipleChildren(['Database:user1.vcf', 'LDAP:user2.vcf']);
+
+ self::assertCount(2, $cards);
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php b/apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php
new file mode 100644
index 00000000000..058735ba32a
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CardDAV\Validation;
+
+use OCA\DAV\CardDAV\Validation\CardDavValidatePlugin;
+use OCP\IAppConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+class CardDavValidatePluginTest extends TestCase {
+
+ private CardDavValidatePlugin $plugin;
+ private IAppConfig&MockObject $config;
+ private RequestInterface&MockObject $request;
+ private ResponseInterface&MockObject $response;
+
+ protected function setUp(): void {
+ parent::setUp();
+ // construct mock objects
+ $this->config = $this->createMock(IAppConfig::class);
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->response = $this->createMock(ResponseInterface::class);
+ $this->plugin = new CardDavValidatePlugin(
+ $this->config,
+ );
+ }
+
+ public function testPutSizeLessThenLimit(): void {
+
+ // construct method responses
+ $this->config
+ ->method('getValueInt')
+ ->with('dav', 'card_size_limit', 5242880)
+ ->willReturn(5242880);
+ $this->request
+ ->method('getRawServerValue')
+ ->with('CONTENT_LENGTH')
+ ->willReturn('1024');
+ // test condition
+ $this->assertTrue(
+ $this->plugin->beforePut($this->request, $this->response)
+ );
+
+ }
+
+ public function testPutSizeMoreThenLimit(): void {
+
+ // construct method responses
+ $this->config
+ ->method('getValueInt')
+ ->with('dav', 'card_size_limit', 5242880)
+ ->willReturn(5242880);
+ $this->request
+ ->method('getRawServerValue')
+ ->with('CONTENT_LENGTH')
+ ->willReturn('6242880');
+ $this->expectException(Forbidden::class);
+ // test condition
+ $this->plugin->beforePut($this->request, $this->response);
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/Command/DeleteCalendarTest.php b/apps/dav/tests/unit/Command/DeleteCalendarTest.php
new file mode 100644
index 00000000000..2bd269de6dc
--- /dev/null
+++ b/apps/dav/tests/unit/Command/DeleteCalendarTest.php
@@ -0,0 +1,231 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Command;
+
+use OCA\DAV\CalDAV\BirthdayService;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Command\DeleteCalendar;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+/**
+ * Class DeleteCalendarTest
+ *
+ * @package OCA\DAV\Tests\Command
+ */
+class DeleteCalendarTest extends TestCase {
+ public const USER = 'user';
+ public const NAME = 'calendar';
+
+ private CalDavBackend&MockObject $calDav;
+ private IConfig&MockObject $config;
+ private IL10N&MockObject $l10n;
+ private IUserManager&MockObject $userManager;
+ private LoggerInterface&MockObject $logger;
+ private DeleteCalendar $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calDav = $this->createMock(CalDavBackend::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->command = new DeleteCalendar(
+ $this->calDav,
+ $this->config,
+ $this->l10n,
+ $this->userManager,
+ $this->logger
+ );
+ }
+
+ public function testInvalidUser(): void {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ 'User <' . self::USER . '> is unknown.');
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USER)
+ ->willReturn(false);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USER,
+ 'name' => self::NAME,
+ ]);
+ }
+
+ public function testNoCalendarName(): void {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ 'Please specify a calendar name or --birthday');
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USER)
+ ->willReturn(true);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USER,
+ ]);
+ }
+
+ public function testInvalidCalendar(): void {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ 'User <' . self::USER . '> has no calendar named <' . self::NAME . '>.');
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USER)
+ ->willReturn(true);
+ $this->calDav->expects($this->once())
+ ->method('getCalendarByUri')
+ ->with(
+ 'principals/users/' . self::USER,
+ self::NAME
+ )
+ ->willReturn(null);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USER,
+ 'name' => self::NAME,
+ ]);
+ }
+
+ public function testDelete(): void {
+ $id = 1234;
+ $calendar = [
+ 'id' => $id,
+ 'principaluri' => 'principals/users/' . self::USER,
+ 'uri' => self::NAME,
+ ];
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USER)
+ ->willReturn(true);
+ $this->calDav->expects($this->once())
+ ->method('getCalendarByUri')
+ ->with(
+ 'principals/users/' . self::USER,
+ self::NAME
+ )
+ ->willReturn($calendar);
+ $this->calDav->expects($this->once())
+ ->method('deleteCalendar')
+ ->with($id, false);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USER,
+ 'name' => self::NAME,
+ ]);
+ }
+
+ public function testForceDelete(): void {
+ $id = 1234;
+ $calendar = [
+ 'id' => $id,
+ 'principaluri' => 'principals/users/' . self::USER,
+ 'uri' => self::NAME
+ ];
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USER)
+ ->willReturn(true);
+ $this->calDav->expects($this->once())
+ ->method('getCalendarByUri')
+ ->with(
+ 'principals/users/' . self::USER,
+ self::NAME
+ )
+ ->willReturn($calendar);
+ $this->calDav->expects($this->once())
+ ->method('deleteCalendar')
+ ->with($id, true);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USER,
+ 'name' => self::NAME,
+ '-f' => true
+ ]);
+ }
+
+ public function testDeleteBirthday(): void {
+ $id = 1234;
+ $calendar = [
+ 'id' => $id,
+ 'principaluri' => 'principals/users/' . self::USER,
+ 'uri' => BirthdayService::BIRTHDAY_CALENDAR_URI,
+ '{DAV:}displayname' => 'Test',
+ ];
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USER)
+ ->willReturn(true);
+ $this->calDav->expects($this->once())
+ ->method('getCalendarByUri')
+ ->with(
+ 'principals/users/' . self::USER,
+ BirthdayService::BIRTHDAY_CALENDAR_URI
+ )
+ ->willReturn($calendar);
+ $this->calDav->expects($this->once())
+ ->method('deleteCalendar')
+ ->with($id);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USER,
+ '--birthday' => true,
+ ]);
+ }
+
+ public function testBirthdayHasPrecedence(): void {
+ $calendar = [
+ 'id' => 1234,
+ 'principaluri' => 'principals/users/' . self::USER,
+ 'uri' => BirthdayService::BIRTHDAY_CALENDAR_URI,
+ '{DAV:}displayname' => 'Test',
+ ];
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USER)
+ ->willReturn(true);
+ $this->calDav->expects($this->once())
+ ->method('getCalendarByUri')
+ ->with(
+ 'principals/users/' . self::USER,
+ BirthdayService::BIRTHDAY_CALENDAR_URI
+ )
+ ->willReturn($calendar);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USER,
+ 'name' => self::NAME,
+ '--birthday' => true,
+ ]);
+ }
+}
diff --git a/apps/dav/tests/unit/Command/ListAddressbooksTest.php b/apps/dav/tests/unit/Command/ListAddressbooksTest.php
new file mode 100644
index 00000000000..2768ed576c3
--- /dev/null
+++ b/apps/dav/tests/unit/Command/ListAddressbooksTest.php
@@ -0,0 +1,107 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Command;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\Command\ListAddressbooks;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+/**
+ * Class ListCalendarsTest
+ *
+ * @package OCA\DAV\Tests\Command
+ */
+class ListAddressbooksTest extends TestCase {
+ private IUserManager&MockObject $userManager;
+ private CardDavBackend&MockObject $cardDavBackend;
+ private ListAddressbooks $command;
+
+ public const USERNAME = 'username';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->cardDavBackend = $this->createMock(CardDavBackend::class);
+
+ $this->command = new ListAddressbooks(
+ $this->userManager,
+ $this->cardDavBackend
+ );
+ }
+
+ public function testWithBadUser(): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USERNAME)
+ ->willReturn(false);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USERNAME,
+ ]);
+ $this->assertStringContainsString('User <' . self::USERNAME . '> in unknown', $commandTester->getDisplay());
+ }
+
+ public function testWithCorrectUserWithNoCalendars(): void {
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USERNAME)
+ ->willReturn(true);
+
+ $this->cardDavBackend->expects($this->once())
+ ->method('getAddressBooksForUser')
+ ->with('principals/users/' . self::USERNAME)
+ ->willReturn([]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USERNAME,
+ ]);
+ $this->assertStringContainsString('User <' . self::USERNAME . "> has no addressbooks\n", $commandTester->getDisplay());
+ }
+
+ public static function dataExecute(): array {
+ return [
+ [false, '✓'],
+ [true, 'x']
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecute')]
+ public function testWithCorrectUser(bool $readOnly, string $output): void {
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USERNAME)
+ ->willReturn(true);
+
+ $this->cardDavBackend->expects($this->once())
+ ->method('getAddressBooksForUser')
+ ->with('principals/users/' . self::USERNAME)
+ ->willReturn([
+ [
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => $readOnly,
+ 'uri' => 'test',
+ '{DAV:}displayname' => 'dp',
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => 'owner-principal',
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname' => 'owner-dp',
+ ]
+ ]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USERNAME,
+ ]);
+ $this->assertStringContainsString($output, $commandTester->getDisplay());
+ }
+}
diff --git a/apps/dav/tests/unit/Command/ListCalendarSharesTest.php b/apps/dav/tests/unit/Command/ListCalendarSharesTest.php
new file mode 100644
index 00000000000..e5d4251cbf9
--- /dev/null
+++ b/apps/dav/tests/unit/Command/ListCalendarSharesTest.php
@@ -0,0 +1,172 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Command\ListCalendarShares;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+class ListCalendarSharesTest extends TestCase {
+
+ private IUserManager&MockObject $userManager;
+ private Principal&MockObject $principal;
+ private CalDavBackend&MockObject $caldav;
+ private SharingMapper $sharingMapper;
+ private ListCalendarShares $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->principal = $this->createMock(Principal::class);
+ $this->caldav = $this->createMock(CalDavBackend::class);
+ $this->sharingMapper = $this->createMock(SharingMapper::class);
+
+ $this->command = new ListCalendarShares(
+ $this->userManager,
+ $this->principal,
+ $this->caldav,
+ $this->sharingMapper,
+ );
+ }
+
+ public function testUserUnknown(): void {
+ $user = 'bob';
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage("User $user is unknown");
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with($user)
+ ->willReturn(false);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => $user,
+ ]);
+ }
+
+ public function testPrincipalNotFound(): void {
+ $user = 'bob';
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage("Unable to fetch principal for user $user");
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with($user)
+ ->willReturn(true);
+
+ $this->principal->expects($this->once())
+ ->method('getPrincipalByPath')
+ ->with('principals/users/' . $user)
+ ->willReturn(null);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => $user,
+ ]);
+ }
+
+ public function testNoCalendarShares(): void {
+ $user = 'bob';
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with($user)
+ ->willReturn(true);
+
+ $this->principal->expects($this->once())
+ ->method('getPrincipalByPath')
+ ->with('principals/users/' . $user)
+ ->willReturn([
+ 'uri' => 'principals/users/' . $user,
+ ]);
+
+ $this->principal->expects($this->once())
+ ->method('getGroupMembership')
+ ->willReturn([]);
+ $this->principal->expects($this->once())
+ ->method('getCircleMembership')
+ ->willReturn([]);
+
+ $this->sharingMapper->expects($this->once())
+ ->method('getSharesByPrincipals')
+ ->willReturn([]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => $user,
+ ]);
+
+ $this->assertStringContainsString(
+ "User $user has no calendar shares",
+ $commandTester->getDisplay()
+ );
+ }
+
+ public function testFilterByCalendarId(): void {
+ $user = 'bob';
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with($user)
+ ->willReturn(true);
+
+ $this->principal->expects($this->once())
+ ->method('getPrincipalByPath')
+ ->with('principals/users/' . $user)
+ ->willReturn([
+ 'uri' => 'principals/users/' . $user,
+ ]);
+
+ $this->principal->expects($this->once())
+ ->method('getGroupMembership')
+ ->willReturn([]);
+ $this->principal->expects($this->once())
+ ->method('getCircleMembership')
+ ->willReturn([]);
+
+ $this->sharingMapper->expects($this->once())
+ ->method('getSharesByPrincipals')
+ ->willReturn([
+ [
+ 'id' => 1000,
+ 'principaluri' => 'principals/users/bob',
+ 'type' => 'calendar',
+ 'access' => 2,
+ 'resourceid' => 10
+ ],
+ [
+ 'id' => 1001,
+ 'principaluri' => 'principals/users/bob',
+ 'type' => 'calendar',
+ 'access' => 3,
+ 'resourceid' => 11
+ ],
+ ]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => $user,
+ '--calendar-id' => 10,
+ ]);
+
+ $this->assertStringNotContainsString(
+ '1001',
+ $commandTester->getDisplay()
+ );
+ }
+}
diff --git a/apps/dav/tests/unit/Command/ListCalendarsTest.php b/apps/dav/tests/unit/Command/ListCalendarsTest.php
new file mode 100644
index 00000000000..d398a7c772f
--- /dev/null
+++ b/apps/dav/tests/unit/Command/ListCalendarsTest.php
@@ -0,0 +1,112 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Command;
+
+use OCA\DAV\CalDAV\BirthdayService;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Command\ListCalendars;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+/**
+ * Class ListCalendarsTest
+ *
+ * @package OCA\DAV\Tests\Command
+ */
+class ListCalendarsTest extends TestCase {
+ private IUserManager&MockObject $userManager;
+ private CalDavBackend&MockObject $calDav;
+ private ListCalendars $command;
+
+ public const USERNAME = 'username';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->calDav = $this->createMock(CalDavBackend::class);
+
+ $this->command = new ListCalendars(
+ $this->userManager,
+ $this->calDav
+ );
+ }
+
+ public function testWithBadUser(): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USERNAME)
+ ->willReturn(false);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USERNAME,
+ ]);
+ $this->assertStringContainsString('User <' . self::USERNAME . '> in unknown', $commandTester->getDisplay());
+ }
+
+ public function testWithCorrectUserWithNoCalendars(): void {
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USERNAME)
+ ->willReturn(true);
+
+ $this->calDav->expects($this->once())
+ ->method('getCalendarsForUser')
+ ->with('principals/users/' . self::USERNAME)
+ ->willReturn([]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USERNAME,
+ ]);
+ $this->assertStringContainsString('User <' . self::USERNAME . "> has no calendars\n", $commandTester->getDisplay());
+ }
+
+ public static function dataExecute(): array {
+ return [
+ [false, '✓'],
+ [true, 'x']
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecute')]
+ public function testWithCorrectUser(bool $readOnly, string $output): void {
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USERNAME)
+ ->willReturn(true);
+
+ $this->calDav->expects($this->once())
+ ->method('getCalendarsForUser')
+ ->with('principals/users/' . self::USERNAME)
+ ->willReturn([
+ [
+ 'uri' => BirthdayService::BIRTHDAY_CALENDAR_URI,
+ ],
+ [
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => $readOnly,
+ 'uri' => 'test',
+ '{DAV:}displayname' => 'dp',
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => 'owner-principal',
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname' => 'owner-dp',
+ ]
+ ]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USERNAME,
+ ]);
+ $this->assertStringContainsString($output, $commandTester->getDisplay());
+ $this->assertStringNotContainsString(BirthdayService::BIRTHDAY_CALENDAR_URI, $commandTester->getDisplay());
+ }
+}
diff --git a/apps/dav/tests/unit/Command/MoveCalendarTest.php b/apps/dav/tests/unit/Command/MoveCalendarTest.php
new file mode 100644
index 00000000000..e9f016961f2
--- /dev/null
+++ b/apps/dav/tests/unit/Command/MoveCalendarTest.php
@@ -0,0 +1,354 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Command;
+
+use InvalidArgumentException;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Command\MoveCalendar;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IUserManager;
+use OCP\Share\IManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+/**
+ * Class MoveCalendarTest
+ *
+ * @package OCA\DAV\Tests\Command
+ */
+class MoveCalendarTest extends TestCase {
+ private IUserManager&MockObject $userManager;
+ private IGroupManager&MockObject $groupManager;
+ private \OCP\Share\IManager&MockObject $shareManager;
+ private IConfig&MockObject $config;
+ private IL10N&MockObject $l10n;
+ private CalDavBackend&MockObject $calDav;
+ private LoggerInterface&MockObject $logger;
+ private MoveCalendar $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->shareManager = $this->createMock(IManager::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->calDav = $this->createMock(CalDavBackend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->command = new MoveCalendar(
+ $this->userManager,
+ $this->groupManager,
+ $this->shareManager,
+ $this->config,
+ $this->l10n,
+ $this->calDav,
+ $this->logger
+ );
+ }
+
+ public static function dataExecute(): array {
+ return [
+ [false, true],
+ [true, false]
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecute')]
+ public function testWithBadUserOrigin(bool $userOriginExists, bool $userDestinationExists): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $this->userManager->expects($this->exactly($userOriginExists ? 2 : 1))
+ ->method('userExists')
+ ->willReturnMap([
+ ['user', $userOriginExists],
+ ['user2', $userDestinationExists],
+ ]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'name' => $this->command->getName(),
+ 'sourceuid' => 'user',
+ 'destinationuid' => 'user2',
+ ]);
+ }
+
+
+ public function testMoveWithInexistantCalendar(): void {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('User <user> has no calendar named <personal>. You can run occ dav:list-calendars to list calendars URIs for this user.');
+
+ $this->userManager->expects($this->exactly(2))
+ ->method('userExists')
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
+ ]);
+
+ $this->calDav->expects($this->once())->method('getCalendarByUri')
+ ->with('principals/users/user', 'personal')
+ ->willReturn(null);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'name' => 'personal',
+ 'sourceuid' => 'user',
+ 'destinationuid' => 'user2',
+ ]);
+ }
+
+
+ public function testMoveWithExistingDestinationCalendar(): void {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('User <user2> already has a calendar named <personal>.');
+
+ $this->userManager->expects($this->exactly(2))
+ ->method('userExists')
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
+ ]);
+
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ ]],
+ ['principals/users/user2', 'personal', [
+ 'id' => 1234,
+ ]],
+ ]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'name' => 'personal',
+ 'sourceuid' => 'user',
+ 'destinationuid' => 'user2',
+ ]);
+ }
+
+ public function testMove(): void {
+ $this->userManager->expects($this->exactly(2))
+ ->method('userExists')
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
+ ]);
+
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ ]],
+ ['principals/users/user2', 'personal', null],
+ ]);
+
+ $this->calDav->expects($this->once())->method('getShares')
+ ->with(1234)
+ ->willReturn([]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'name' => 'personal',
+ 'sourceuid' => 'user',
+ 'destinationuid' => 'user2',
+ ]);
+
+ $this->assertStringContainsString('[OK] Calendar <personal> was moved from user <user> to <user2>', $commandTester->getDisplay());
+ }
+
+ public static function dataTestMoveWithDestinationNotPartOfGroup(): array {
+ return [
+ [true],
+ [false]
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestMoveWithDestinationNotPartOfGroup')]
+ public function testMoveWithDestinationNotPartOfGroup(bool $shareWithGroupMembersOnly): void {
+ $this->userManager->expects($this->exactly(2))
+ ->method('userExists')
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
+ ]);
+
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ 'uri' => 'personal',
+ ]],
+ ['principals/users/user2', 'personal', null],
+ ]);
+
+ $this->shareManager->expects($this->once())->method('shareWithGroupMembersOnly')
+ ->willReturn($shareWithGroupMembersOnly);
+
+ $this->calDav->expects($this->once())->method('getShares')
+ ->with(1234)
+ ->willReturn([
+ ['href' => 'principal:principals/groups/nextclouders']
+ ]);
+ if ($shareWithGroupMembersOnly === true) {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('User <user2> is not part of the group <nextclouders> with whom the calendar <personal> was shared. You may use -f to move the calendar while deleting this share.');
+ }
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'name' => 'personal',
+ 'sourceuid' => 'user',
+ 'destinationuid' => 'user2',
+ ]);
+ }
+
+ public function testMoveWithDestinationPartOfGroup(): void {
+ $this->userManager->expects($this->exactly(2))
+ ->method('userExists')
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
+ ]);
+
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ 'uri' => 'personal',
+ ]],
+ ['principals/users/user2', 'personal', null],
+ ]);
+
+ $this->shareManager->expects($this->once())->method('shareWithGroupMembersOnly')
+ ->willReturn(true);
+
+ $this->calDav->expects($this->once())->method('getShares')
+ ->with(1234)
+ ->willReturn([
+ ['href' => 'principal:principals/groups/nextclouders']
+ ]);
+
+ $this->groupManager->expects($this->once())->method('isInGroup')
+ ->with('user2', 'nextclouders')
+ ->willReturn(true);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'name' => 'personal',
+ 'sourceuid' => 'user',
+ 'destinationuid' => 'user2',
+ ]);
+
+ $this->assertStringContainsString('[OK] Calendar <personal> was moved from user <user> to <user2>', $commandTester->getDisplay());
+ }
+
+ public function testMoveWithDestinationNotPartOfGroupAndForce(): void {
+ $this->userManager->expects($this->exactly(2))
+ ->method('userExists')
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
+ ]);
+
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ 'uri' => 'personal',
+ '{DAV:}displayname' => 'Personal'
+ ]],
+ ['principals/users/user2', 'personal', null],
+ ]);
+
+ $this->shareManager->expects($this->once())->method('shareWithGroupMembersOnly')
+ ->willReturn(true);
+
+ $this->calDav->expects($this->once())->method('getShares')
+ ->with(1234)
+ ->willReturn([
+ [
+ 'href' => 'principal:principals/groups/nextclouders',
+ '{DAV:}displayname' => 'Personal'
+ ]
+ ]);
+ $this->calDav->expects($this->once())->method('updateShares');
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'name' => 'personal',
+ 'sourceuid' => 'user',
+ 'destinationuid' => 'user2',
+ '--force' => true
+ ]);
+
+ $this->assertStringContainsString('[OK] Calendar <personal> was moved from user <user> to <user2>', $commandTester->getDisplay());
+ }
+
+ public static function dataTestMoveWithCalendarAlreadySharedToDestination(): array {
+ return [
+ [true],
+ [false]
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestMoveWithCalendarAlreadySharedToDestination')]
+ public function testMoveWithCalendarAlreadySharedToDestination(bool $force): void {
+ $this->userManager->expects($this->exactly(2))
+ ->method('userExists')
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
+ ]);
+
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ 'uri' => 'personal',
+ '{DAV:}displayname' => 'Personal'
+ ]],
+ ['principals/users/user2', 'personal', null],
+ ]);
+
+ $this->calDav->expects($this->once())->method('getShares')
+ ->with(1234)
+ ->willReturn([
+ [
+ 'href' => 'principal:principals/users/user2',
+ '{DAV:}displayname' => 'Personal'
+ ]
+ ]);
+
+ if ($force === false) {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The calendar <personal> is already shared to user <user2>.You may use -f to move the calendar while deleting this share.');
+ } else {
+ $this->calDav->expects($this->once())->method('updateShares');
+ }
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'name' => 'personal',
+ 'sourceuid' => 'user',
+ 'destinationuid' => 'user2',
+ '--force' => $force,
+ ]);
+ }
+}
diff --git a/apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php b/apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php
new file mode 100644
index 00000000000..ec56aa64eb2
--- /dev/null
+++ b/apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2018 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Command;
+
+use OCA\DAV\Command\RemoveInvalidShares;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCP\IDBConnection;
+use OCP\Server;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+/**
+ * Class RemoveInvalidSharesTest
+ *
+ * @package OCA\DAV\Tests\Unit\Repair
+ * @group DB
+ */
+class RemoveInvalidSharesTest extends TestCase {
+ protected function setUp(): void {
+ parent::setUp();
+ $db = Server::get(IDBConnection::class);
+
+ $db->insertIfNotExist('*PREFIX*dav_shares', [
+ 'principaluri' => 'principal:unknown',
+ 'type' => 'calendar',
+ 'access' => 2,
+ 'resourceid' => 666,
+ ]);
+ }
+
+ public function test(): void {
+ $db = Server::get(IDBConnection::class);
+ $principal = $this->createMock(Principal::class);
+
+ $repair = new RemoveInvalidShares($db, $principal);
+ $this->invokePrivate($repair, 'run', [$this->createMock(InputInterface::class), $this->createMock(OutputInterface::class)]);
+
+ $query = $db->getQueryBuilder();
+ $query->select('*')
+ ->from('dav_shares')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter('principal:unknown')));
+ $result = $query->executeQuery();
+ $data = $result->fetchAll();
+ $result->closeCursor();
+ $this->assertEquals(0, count($data));
+ }
+}
diff --git a/apps/dav/tests/unit/comments/commentnode.php b/apps/dav/tests/unit/Comments/CommentsNodeTest.php
index 8ebc5c2ff2c..9e108b4cf63 100644
--- a/apps/dav/tests/unit/comments/commentnode.php
+++ b/apps/dav/tests/unit/Comments/CommentsNodeTest.php
@@ -1,47 +1,40 @@
<?php
+
+declare(strict_types=1);
/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
-namespace OCA\DAV\Tests\Unit\Comments;
+namespace OCA\DAV\Tests\unit\Comments;
use OCA\DAV\Comments\CommentNode;
use OCP\Comments\IComment;
+use OCP\Comments\ICommentsManager;
use OCP\Comments\MessageTooLongException;
-
-class CommentsNode extends \Test\TestCase {
-
- protected $commentsManager;
- protected $comment;
- protected $node;
- protected $userManager;
- protected $logger;
- protected $userSession;
-
- public function setUp() {
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\PropPatch;
+
+class CommentsNodeTest extends \Test\TestCase {
+ protected ICommentsManager&MockObject $commentsManager;
+ protected IComment&MockObject $comment;
+ protected IUserManager&MockObject $userManager;
+ protected LoggerInterface&MockObject $logger;
+ protected IUserSession&MockObject $userSession;
+ protected CommentNode $node;
+
+ protected function setUp(): void {
parent::setUp();
- $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager');
- $this->comment = $this->getMock('\OCP\Comments\IComment');
- $this->userManager = $this->getMock('\OCP\IUserManager');
- $this->userSession = $this->getMock('\OCP\IUserSession');
- $this->logger = $this->getMock('\OCP\ILogger');
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->comment = $this->createMock(IComment::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
$this->node = new CommentNode(
$this->commentsManager,
@@ -52,28 +45,27 @@ class CommentsNode extends \Test\TestCase {
);
}
- public function testDelete() {
- $user = $this->getMock('\OCP\IUser');
-
+ public function testDelete(): void {
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($user));
+ ->willReturn($user);
$this->comment->expects($this->once())
->method('getId')
- ->will($this->returnValue('19'));
+ ->willReturn('19');
$this->comment->expects($this->any())
->method('getActorType')
- ->will($this->returnValue('users'));
+ ->willReturn('users');
$this->comment->expects($this->any())
->method('getActorId')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
$this->commentsManager->expects($this->once())
->method('delete')
@@ -82,30 +74,29 @@ class CommentsNode extends \Test\TestCase {
$this->node->delete();
}
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testDeleteForbidden() {
- $user = $this->getMock('\OCP\IUser');
+ public function testDeleteForbidden(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
- ->will($this->returnValue('mallory'));
+ ->willReturn('mallory');
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($user));
+ ->willReturn($user);
$this->comment->expects($this->never())
->method('getId');
$this->comment->expects($this->any())
->method('getActorType')
- ->will($this->returnValue('users'));
+ ->willReturn('users');
$this->comment->expects($this->any())
->method('getActorId')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
$this->commentsManager->expects($this->never())
->method('delete');
@@ -113,38 +104,37 @@ class CommentsNode extends \Test\TestCase {
$this->node->delete();
}
- public function testGetName() {
+ public function testGetName(): void {
$id = '19';
$this->comment->expects($this->once())
->method('getId')
- ->will($this->returnValue($id));
+ ->willReturn($id);
$this->assertSame($this->node->getName(), $id);
}
- /**
- * @expectedException \Sabre\DAV\Exception\MethodNotAllowed
- */
- public function testSetName() {
+
+ public function testSetName(): void {
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+
$this->node->setName('666');
}
- public function testGetLastModified() {
+ public function testGetLastModified(): void {
$this->assertSame($this->node->getLastModified(), null);
}
- public function testUpdateComment() {
+ public function testUpdateComment(): void {
$msg = 'Hello Earth';
- $user = $this->getMock('\OCP\IUser');
-
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($user));
+ ->willReturn($user);
$this->comment->expects($this->once())
->method('setMessage')
@@ -152,11 +142,11 @@ class CommentsNode extends \Test\TestCase {
$this->comment->expects($this->any())
->method('getActorType')
- ->will($this->returnValue('users'));
+ ->willReturn('users');
$this->comment->expects($this->any())
->method('getActorId')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
$this->commentsManager->expects($this->once())
->method('save')
@@ -165,104 +155,105 @@ class CommentsNode extends \Test\TestCase {
$this->assertTrue($this->node->updateComment($msg));
}
- public function testUpdateCommentLogException() {
- $msg = null;
- $user = $this->getMock('\OCP\IUser');
+ public function testUpdateCommentLogException(): void {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('buh!');
+
+ $msg = null;
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($user));
+ ->willReturn($user);
$this->comment->expects($this->once())
->method('setMessage')
->with($msg)
- ->will($this->throwException(new \Exception('buh!')));
+ ->willThrowException(new \Exception('buh!'));
$this->comment->expects($this->any())
->method('getActorType')
- ->will($this->returnValue('users'));
+ ->willReturn('users');
$this->comment->expects($this->any())
->method('getActorId')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
$this->commentsManager->expects($this->never())
->method('save');
$this->logger->expects($this->once())
- ->method('logException');
+ ->method('error');
- $this->assertFalse($this->node->updateComment($msg));
+ $this->node->updateComment($msg);
}
- /**
- * @expectedException \Sabre\DAV\Exception\BadRequest
- * @expectedExceptionMessage Message exceeds allowed character limit of
- */
- public function testUpdateCommentMessageTooLongException() {
- $user = $this->getMock('\OCP\IUser');
+ public function testUpdateCommentMessageTooLongException(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+ $this->expectExceptionMessage('Message exceeds allowed character limit of');
+
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($user));
+ ->willReturn($user);
$this->comment->expects($this->once())
->method('setMessage')
- ->will($this->throwException(new MessageTooLongException()));
+ ->willThrowException(new MessageTooLongException());
$this->comment->expects($this->any())
->method('getActorType')
- ->will($this->returnValue('users'));
+ ->willReturn('users');
$this->comment->expects($this->any())
->method('getActorId')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
$this->commentsManager->expects($this->never())
->method('save');
$this->logger->expects($this->once())
- ->method('logException');
+ ->method('error');
// imagine 'foo' has >1k characters. comment is mocked anyway.
$this->node->updateComment('foo');
}
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testUpdateForbiddenByUser() {
- $msg = 'HaXX0r';
- $user = $this->getMock('\OCP\IUser');
+ public function testUpdateForbiddenByUser(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $msg = 'HaXX0r';
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
- ->will($this->returnValue('mallory'));
+ ->willReturn('mallory');
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($user));
+ ->willReturn($user);
$this->comment->expects($this->never())
->method('setMessage');
$this->comment->expects($this->any())
->method('getActorType')
- ->will($this->returnValue('users'));
+ ->willReturn('users');
$this->comment->expects($this->any())
->method('getActorId')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
$this->commentsManager->expects($this->never())
->method('save');
@@ -270,27 +261,26 @@ class CommentsNode extends \Test\TestCase {
$this->node->updateComment($msg);
}
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testUpdateForbiddenByType() {
- $msg = 'HaXX0r';
- $user = $this->getMock('\OCP\IUser');
+ public function testUpdateForbiddenByType(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $msg = 'HaXX0r';
+
+ $user = $this->createMock(IUser::class);
$user->expects($this->never())
->method('getUID');
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($user));
+ ->willReturn($user);
$this->comment->expects($this->never())
->method('setMessage');
$this->comment->expects($this->any())
->method('getActorType')
- ->will($this->returnValue('bots'));
+ ->willReturn('bots');
$this->commentsManager->expects($this->never())
->method('save');
@@ -298,22 +288,22 @@ class CommentsNode extends \Test\TestCase {
$this->node->updateComment($msg);
}
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testUpdateForbiddenByNotLoggedIn() {
+
+ public function testUpdateForbiddenByNotLoggedIn(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
$msg = 'HaXX0r';
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue(null));
+ ->willReturn(null);
$this->comment->expects($this->never())
->method('setMessage');
$this->comment->expects($this->any())
->method('getActorType')
- ->will($this->returnValue('users'));
+ ->willReturn('users');
$this->commentsManager->expects($this->never())
->method('save');
@@ -321,11 +311,8 @@ class CommentsNode extends \Test\TestCase {
$this->node->updateComment($msg);
}
- public function testPropPatch() {
- $propPatch = $this->getMockBuilder('Sabre\DAV\PropPatch')
- ->disableOriginalConstructor()
- ->getMock();
-
+ public function testPropPatch(): void {
+ $propPatch = $this->createMock(PropPatch::class);
$propPatch->expects($this->once())
->method('handle')
->with('{http://owncloud.org/ns}message');
@@ -333,7 +320,7 @@ class CommentsNode extends \Test\TestCase {
$this->node->propPatch($propPatch);
}
- public function testGetProperties() {
+ public function testGetProperties(): void {
$ns = '{http://owncloud.org/ns}';
$expected = [
$ns . 'id' => '123',
@@ -341,6 +328,18 @@ class CommentsNode extends \Test\TestCase {
$ns . 'topmostParentId' => '2',
$ns . 'childrenCount' => 3,
$ns . 'message' => 'such a nice file you have…',
+ $ns . 'mentions' => [
+ [ $ns . 'mention' => [
+ $ns . 'mentionType' => 'user',
+ $ns . 'mentionId' => 'alice',
+ $ns . 'mentionDisplayName' => 'Alice Al-Isson',
+ ] ],
+ [ $ns . 'mention' => [
+ $ns . 'mentionType' => 'user',
+ $ns . 'mentionId' => 'bob',
+ $ns . 'mentionDisplayName' => 'Unknown user',
+ ] ],
+ ],
$ns . 'verb' => 'comment',
$ns . 'actorType' => 'users',
$ns . 'actorId' => 'alice',
@@ -349,84 +348,120 @@ class CommentsNode extends \Test\TestCase {
$ns . 'latestChildDateTime' => new \DateTime('2016-01-12 18:48:00'),
$ns . 'objectType' => 'files',
$ns . 'objectId' => '1848',
+ $ns . 'referenceId' => 'ref',
$ns . 'isUnread' => null,
+ $ns . 'reactions' => [],
+ $ns . 'metaData' => [
+ 'last_edited_at' => 1702553770,
+ 'last_edited_by_id' => 'charly',
+ 'last_edited_by_type' => 'user',
+ ],
+ $ns . 'expireDate' => new \DateTime('2016-01-12 19:00:00'),
];
+ $this->commentsManager->expects($this->exactly(2))
+ ->method('resolveDisplayName')
+ ->willReturnMap([
+ ['user', 'alice', 'Alice Al-Isson'],
+ ['user', 'bob', 'Unknown user']
+ ]);
+
$this->comment->expects($this->once())
->method('getId')
- ->will($this->returnValue($expected[$ns . 'id']));
+ ->willReturn($expected[$ns . 'id']);
$this->comment->expects($this->once())
->method('getParentId')
- ->will($this->returnValue($expected[$ns . 'parentId']));
+ ->willReturn($expected[$ns . 'parentId']);
$this->comment->expects($this->once())
->method('getTopmostParentId')
- ->will($this->returnValue($expected[$ns . 'topmostParentId']));
+ ->willReturn($expected[$ns . 'topmostParentId']);
$this->comment->expects($this->once())
->method('getChildrenCount')
- ->will($this->returnValue($expected[$ns . 'childrenCount']));
+ ->willReturn($expected[$ns . 'childrenCount']);
$this->comment->expects($this->once())
->method('getMessage')
- ->will($this->returnValue($expected[$ns . 'message']));
+ ->willReturn($expected[$ns . 'message']);
+
+ $this->comment->expects($this->once())
+ ->method('getMentions')
+ ->willReturn([
+ ['type' => 'user', 'id' => 'alice'],
+ ['type' => 'user', 'id' => 'bob'],
+ ]);
$this->comment->expects($this->once())
->method('getVerb')
- ->will($this->returnValue($expected[$ns . 'verb']));
+ ->willReturn($expected[$ns . 'verb']);
$this->comment->expects($this->exactly(2))
->method('getActorType')
- ->will($this->returnValue($expected[$ns . 'actorType']));
+ ->willReturn($expected[$ns . 'actorType']);
$this->comment->expects($this->exactly(2))
->method('getActorId')
- ->will($this->returnValue($expected[$ns . 'actorId']));
+ ->willReturn($expected[$ns . 'actorId']);
$this->comment->expects($this->once())
->method('getCreationDateTime')
- ->will($this->returnValue($expected[$ns . 'creationDateTime']));
+ ->willReturn($expected[$ns . 'creationDateTime']);
$this->comment->expects($this->once())
->method('getLatestChildDateTime')
- ->will($this->returnValue($expected[$ns . 'latestChildDateTime']));
+ ->willReturn($expected[$ns . 'latestChildDateTime']);
$this->comment->expects($this->once())
->method('getObjectType')
- ->will($this->returnValue($expected[$ns . 'objectType']));
+ ->willReturn($expected[$ns . 'objectType']);
$this->comment->expects($this->once())
->method('getObjectId')
- ->will($this->returnValue($expected[$ns . 'objectId']));
+ ->willReturn($expected[$ns . 'objectId']);
+
+ $this->comment->expects($this->once())
+ ->method('getReferenceId')
+ ->willReturn($expected[$ns . 'referenceId']);
+
+ $this->comment->expects($this->once())
+ ->method('getMetaData')
+ ->willReturn($expected[$ns . 'metaData']);
- $user = $this->getMockBuilder('\OCP\IUser')
+ $this->comment->expects($this->once())
+ ->method('getExpireDate')
+ ->willReturn($expected[$ns . 'expireDate']);
+
+ $user = $this->getMockBuilder(IUser::class)
->disableOriginalConstructor()
->getMock();
$user->expects($this->once())
->method('getDisplayName')
- ->will($this->returnValue($expected[$ns . 'actorDisplayName']));
+ ->willReturn($expected[$ns . 'actorDisplayName']);
$this->userManager->expects($this->once())
->method('get')
->with('alice')
- ->will($this->returnValue($user));
+ ->willReturn($user);
$properties = $this->node->getProperties(null);
- foreach($properties as $name => $value) {
- $this->assertTrue(array_key_exists($name, $expected));
+ foreach ($properties as $name => $value) {
+ $this->assertArrayHasKey($name, $expected, 'Key not found in the list of $expected');
$this->assertSame($expected[$name], $value);
unset($expected[$name]);
}
$this->assertTrue(empty($expected));
}
- public function readCommentProvider() {
+ public static function readCommentProvider(): array {
$creationDT = new \DateTime('2016-01-19 18:48:00');
$diff = new \DateInterval('PT2H');
- $readDT1 = clone $creationDT; $readDT1->sub($diff);
- $readDT2 = clone $creationDT; $readDT2->add($diff);
+ $readDT1 = clone $creationDT;
+ $readDT1->sub($diff);
+ $readDT2 = clone $creationDT;
+ $readDT2->add($diff);
return [
[$creationDT, $readDT1, 'true'],
[$creationDT, $readDT2, 'false'],
@@ -434,22 +469,27 @@ class CommentsNode extends \Test\TestCase {
];
}
- /**
- * @dataProvider readCommentProvider
- * @param $expected
- */
- public function testGetPropertiesUnreadProperty($creationDT, $readDT, $expected) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('readCommentProvider')]
+ public function testGetPropertiesUnreadProperty(\DateTime $creationDT, ?\DateTime $readDT, string $expected): void {
$this->comment->expects($this->any())
->method('getCreationDateTime')
- ->will($this->returnValue($creationDT));
+ ->willReturn($creationDT);
+
+ $this->comment->expects($this->any())
+ ->method('getMentions')
+ ->willReturn([]);
$this->commentsManager->expects($this->once())
->method('getReadMark')
- ->will($this->returnValue($readDT));
+ ->willReturn($readDT);
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($this->getMock('\OCP\IUser')));
+ ->willReturn(
+ $this->getMockBuilder(IUser::class)
+ ->disableOriginalConstructor()
+ ->getMock()
+ );
$properties = $this->node->getProperties(null);
diff --git a/apps/dav/tests/unit/comments/commentsplugin.php b/apps/dav/tests/unit/Comments/CommentsPluginTest.php
index c7d073d1491..18d32772f7b 100644
--- a/apps/dav/tests/unit/comments/commentsplugin.php
+++ b/apps/dav/tests/unit/Comments/CommentsPluginTest.php
@@ -1,66 +1,48 @@
<?php
+
/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
-namespace OCA\DAV\Tests\Unit\Comments;
+namespace OCA\DAV\Tests\unit\Comments;
use OC\Comments\Comment;
use OCA\DAV\Comments\CommentsPlugin as CommentsPluginImplementation;
+use OCA\DAV\Comments\EntityCollection;
use OCP\Comments\IComment;
-use Sabre\DAV\Exception\NotFound;
-
-class CommentsPlugin extends \Test\TestCase {
- /** @var \Sabre\DAV\Server */
- private $server;
-
- /** @var \Sabre\DAV\Tree */
- private $tree;
-
- /** @var \OCP\Comments\ICommentsManager */
- private $commentsManager;
-
- /** @var \OCP\IUserSession */
- private $userSession;
-
- /** @var CommentsPluginImplementation */
- private $plugin;
-
- public function setUp() {
+use OCP\Comments\ICommentsManager;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\INode;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+class CommentsPluginTest extends \Test\TestCase {
+ private \Sabre\DAV\Server&MockObject $server;
+ private Tree&MockObject $tree;
+ private ICommentsManager&MockObject $commentsManager;
+ private IUserSession&MockObject $userSession;
+ private CommentsPluginImplementation $plugin;
+
+ protected function setUp(): void {
parent::setUp();
- $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
- ->disableOriginalConstructor()
- ->getMock();
+ $this->tree = $this->createMock(Tree::class);
- $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
+ $this->server = $this->getMockBuilder(\Sabre\DAV\Server::class)
->setConstructorArgs([$this->tree])
- ->setMethods(['getRequestUri'])
+ ->onlyMethods(['getRequestUri'])
->getMock();
- $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager');
- $this->userSession = $this->getMock('\OCP\IUserSession');
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
$this->plugin = new CommentsPluginImplementation($this->commentsManager, $this->userSession);
}
- public function testCreateComment() {
+ public function testCreateComment(): void {
$commentData = [
'actorType' => 'users',
'verb' => 'comment',
@@ -79,20 +61,22 @@ class CommentsPlugin extends \Test\TestCase {
$requestData = json_encode($commentData);
- $user = $this->getMock('OCP\IUser');
+ $user = $this->getMockBuilder(IUser::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$user->expects($this->once())
->method('getUID')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
- $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection')
+ $node = $this->getMockBuilder(EntityCollection::class)
->disableOriginalConstructor()
->getMock();
$node->expects($this->once())
->method('getName')
- ->will($this->returnValue('files'));
+ ->willReturn('files');
$node->expects($this->once())
->method('getId')
- ->will($this->returnValue('42'));
+ ->willReturn('42');
$node->expects($this->once())
->method('setReadMarker')
@@ -101,11 +85,11 @@ class CommentsPlugin extends \Test\TestCase {
$this->commentsManager->expects($this->once())
->method('create')
->with('users', 'alice', 'files', '42')
- ->will($this->returnValue($comment));
+ ->willReturn($comment);
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($user));
+ ->willReturn($user);
// technically, this is a shortcut. Inbetween EntityTypeCollection would
// be returned, but doing it exactly right would not be really
@@ -114,32 +98,32 @@ class CommentsPlugin extends \Test\TestCase {
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->returnValue($node));
+ ->willReturn($node);
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ $request = $this->getMockBuilder(RequestInterface::class)
->disableOriginalConstructor()
->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
+ $response = $this->getMockBuilder(ResponseInterface::class)
->disableOriginalConstructor()
->getMock();
$request->expects($this->once())
->method('getPath')
- ->will($this->returnValue('/' . $path));
+ ->willReturn('/' . $path);
$request->expects($this->once())
->method('getBodyAsString')
- ->will($this->returnValue($requestData));
+ ->willReturn($requestData);
$request->expects($this->once())
->method('getHeader')
->with('Content-Type')
- ->will($this->returnValue('application/json'));
+ ->willReturn('application/json');
$request->expects($this->once())
->method('getUrl')
- ->will($this->returnValue('http://example.com/dav/' . $path));
+ ->willReturn('http://example.com/dav/' . $path);
$response->expects($this->once())
->method('setHeader')
@@ -147,16 +131,16 @@ class CommentsPlugin extends \Test\TestCase {
$this->server->expects($this->any())
->method('getRequestUri')
- ->will($this->returnValue($path));
+ ->willReturn($path);
$this->plugin->initialize($this->server);
$this->plugin->httpPost($request, $response);
}
- /**
- * @expectedException \Sabre\DAV\Exception\NotFound
- */
- public function testCreateCommentInvalidObject() {
+
+ public function testCreateCommentInvalidObject(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
$commentData = [
'actorType' => 'users',
'verb' => 'comment',
@@ -164,20 +148,22 @@ class CommentsPlugin extends \Test\TestCase {
];
$comment = new Comment([
- 'objectType' => 'files',
- 'objectId' => '666',
- 'actorType' => 'users',
- 'actorId' => 'alice'
- ] + $commentData);
+ 'objectType' => 'files',
+ 'objectId' => '666',
+ 'actorType' => 'users',
+ 'actorId' => 'alice'
+ ] + $commentData);
$comment->setId('23');
$path = 'comments/files/666';
- $user = $this->getMock('OCP\IUser');
+ $user = $this->getMockBuilder(IUser::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$user->expects($this->never())
->method('getUID');
- $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection')
+ $node = $this->getMockBuilder(EntityCollection::class)
->disableOriginalConstructor()
->getMock();
$node->expects($this->never())
@@ -198,19 +184,19 @@ class CommentsPlugin extends \Test\TestCase {
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->throwException(new \Sabre\DAV\Exception\NotFound()));
+ ->willThrowException(new \Sabre\DAV\Exception\NotFound());
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ $request = $this->getMockBuilder(RequestInterface::class)
->disableOriginalConstructor()
->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
+ $response = $this->getMockBuilder(ResponseInterface::class)
->disableOriginalConstructor()
->getMock();
$request->expects($this->once())
->method('getPath')
- ->will($this->returnValue('/' . $path));
+ ->willReturn('/' . $path);
$request->expects($this->never())
->method('getBodyAsString');
@@ -227,16 +213,16 @@ class CommentsPlugin extends \Test\TestCase {
$this->server->expects($this->any())
->method('getRequestUri')
- ->will($this->returnValue($path));
+ ->willReturn($path);
$this->plugin->initialize($this->server);
$this->plugin->httpPost($request, $response);
}
- /**
- * @expectedException \Sabre\DAV\Exception\BadRequest
- */
- public function testCreateCommentInvalidActor() {
+
+ public function testCreateCommentInvalidActor(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+
$commentData = [
'actorType' => 'robots',
'verb' => 'comment',
@@ -244,30 +230,32 @@ class CommentsPlugin extends \Test\TestCase {
];
$comment = new Comment([
- 'objectType' => 'files',
- 'objectId' => '42',
- 'actorType' => 'users',
- 'actorId' => 'alice'
- ] + $commentData);
+ 'objectType' => 'files',
+ 'objectId' => '42',
+ 'actorType' => 'users',
+ 'actorId' => 'alice'
+ ] + $commentData);
$comment->setId('23');
$path = 'comments/files/42';
$requestData = json_encode($commentData);
- $user = $this->getMock('OCP\IUser');
+ $user = $this->getMockBuilder(IUser::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$user->expects($this->never())
->method('getUID');
- $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection')
+ $node = $this->getMockBuilder(EntityCollection::class)
->disableOriginalConstructor()
->getMock();
$node->expects($this->once())
->method('getName')
- ->will($this->returnValue('files'));
+ ->willReturn('files');
$node->expects($this->once())
->method('getId')
- ->will($this->returnValue('42'));
+ ->willReturn('42');
$this->commentsManager->expects($this->never())
->method('create');
@@ -282,28 +270,28 @@ class CommentsPlugin extends \Test\TestCase {
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->returnValue($node));
+ ->willReturn($node);
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ $request = $this->getMockBuilder(RequestInterface::class)
->disableOriginalConstructor()
->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
+ $response = $this->getMockBuilder(ResponseInterface::class)
->disableOriginalConstructor()
->getMock();
$request->expects($this->once())
->method('getPath')
- ->will($this->returnValue('/' . $path));
+ ->willReturn('/' . $path);
$request->expects($this->once())
->method('getBodyAsString')
- ->will($this->returnValue($requestData));
+ ->willReturn($requestData);
$request->expects($this->once())
->method('getHeader')
->with('Content-Type')
- ->will($this->returnValue('application/json'));
+ ->willReturn('application/json');
$request->expects($this->never())
->method('getUrl');
@@ -313,16 +301,16 @@ class CommentsPlugin extends \Test\TestCase {
$this->server->expects($this->any())
->method('getRequestUri')
- ->will($this->returnValue($path));
+ ->willReturn($path);
$this->plugin->initialize($this->server);
$this->plugin->httpPost($request, $response);
}
- /**
- * @expectedException \Sabre\DAV\Exception\UnsupportedMediaType
- */
- public function testCreateCommentUnsupportedMediaType() {
+
+ public function testCreateCommentUnsupportedMediaType(): void {
+ $this->expectException(\Sabre\DAV\Exception\UnsupportedMediaType::class);
+
$commentData = [
'actorType' => 'users',
'verb' => 'comment',
@@ -330,30 +318,32 @@ class CommentsPlugin extends \Test\TestCase {
];
$comment = new Comment([
- 'objectType' => 'files',
- 'objectId' => '42',
- 'actorType' => 'users',
- 'actorId' => 'alice'
- ] + $commentData);
+ 'objectType' => 'files',
+ 'objectId' => '42',
+ 'actorType' => 'users',
+ 'actorId' => 'alice'
+ ] + $commentData);
$comment->setId('23');
$path = 'comments/files/42';
$requestData = json_encode($commentData);
- $user = $this->getMock('OCP\IUser');
+ $user = $this->getMockBuilder(IUser::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$user->expects($this->never())
->method('getUID');
- $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection')
+ $node = $this->getMockBuilder(EntityCollection::class)
->disableOriginalConstructor()
->getMock();
$node->expects($this->once())
->method('getName')
- ->will($this->returnValue('files'));
+ ->willReturn('files');
$node->expects($this->once())
->method('getId')
- ->will($this->returnValue('42'));
+ ->willReturn('42');
$this->commentsManager->expects($this->never())
->method('create');
@@ -368,28 +358,28 @@ class CommentsPlugin extends \Test\TestCase {
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->returnValue($node));
+ ->willReturn($node);
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ $request = $this->getMockBuilder(RequestInterface::class)
->disableOriginalConstructor()
->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
+ $response = $this->getMockBuilder(ResponseInterface::class)
->disableOriginalConstructor()
->getMock();
$request->expects($this->once())
->method('getPath')
- ->will($this->returnValue('/' . $path));
+ ->willReturn('/' . $path);
$request->expects($this->once())
->method('getBodyAsString')
- ->will($this->returnValue($requestData));
+ ->willReturn($requestData);
$request->expects($this->once())
->method('getHeader')
->with('Content-Type')
- ->will($this->returnValue('application/trumpscript'));
+ ->willReturn('application/trumpscript');
$request->expects($this->never())
->method('getUrl');
@@ -399,16 +389,16 @@ class CommentsPlugin extends \Test\TestCase {
$this->server->expects($this->any())
->method('getRequestUri')
- ->will($this->returnValue($path));
+ ->willReturn($path);
$this->plugin->initialize($this->server);
$this->plugin->httpPost($request, $response);
}
- /**
- * @expectedException \Sabre\DAV\Exception\BadRequest
- */
- public function testCreateCommentInvalidPayload() {
+
+ public function testCreateCommentInvalidPayload(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+
$commentData = [
'actorType' => 'users',
'verb' => '',
@@ -416,52 +406,44 @@ class CommentsPlugin extends \Test\TestCase {
];
$comment = new Comment([
- 'objectType' => 'files',
- 'objectId' => '42',
- 'actorType' => 'users',
- 'actorId' => 'alice',
- 'message' => 'dummy',
- 'verb' => 'dummy'
- ]);
+ 'objectType' => 'files',
+ 'objectId' => '42',
+ 'actorType' => 'users',
+ 'actorId' => 'alice',
+ 'message' => 'dummy',
+ 'verb' => 'dummy'
+ ]);
$comment->setId('23');
$path = 'comments/files/42';
$requestData = json_encode($commentData);
- $user = $this->getMock('OCP\IUser');
+ $user = $this->getMockBuilder(IUser::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$user->expects($this->once())
->method('getUID')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
- $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection')
+ $node = $this->getMockBuilder(EntityCollection::class)
->disableOriginalConstructor()
->getMock();
$node->expects($this->once())
->method('getName')
- ->will($this->returnValue('files'));
+ ->willReturn('files');
$node->expects($this->once())
->method('getId')
- ->will($this->returnValue('42'));
+ ->willReturn('42');
$this->commentsManager->expects($this->once())
->method('create')
->with('users', 'alice', 'files', '42')
- ->will($this->returnValue($comment));
-
- $this->commentsManager->expects($this->any())
- ->method('setMessage')
- ->with('')
- ->will($this->throwException(new \InvalidArgumentException()));
-
- $this->commentsManager->expects($this->any())
- ->method('setVerb')
- ->with('')
- ->will($this->throwException(new \InvalidArgumentException()));
+ ->willReturn($comment);
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($user));
+ ->willReturn($user);
// technically, this is a shortcut. Inbetween EntityTypeCollection would
// be returned, but doing it exactly right would not be really
@@ -470,28 +452,28 @@ class CommentsPlugin extends \Test\TestCase {
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->returnValue($node));
+ ->willReturn($node);
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ $request = $this->getMockBuilder(RequestInterface::class)
->disableOriginalConstructor()
->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
+ $response = $this->getMockBuilder(ResponseInterface::class)
->disableOriginalConstructor()
->getMock();
$request->expects($this->once())
->method('getPath')
- ->will($this->returnValue('/' . $path));
+ ->willReturn('/' . $path);
$request->expects($this->once())
->method('getBodyAsString')
- ->will($this->returnValue($requestData));
+ ->willReturn($requestData);
$request->expects($this->once())
->method('getHeader')
->with('Content-Type')
- ->will($this->returnValue('application/json'));
+ ->willReturn('application/json');
$request->expects($this->never())
->method('getUrl');
@@ -501,17 +483,17 @@ class CommentsPlugin extends \Test\TestCase {
$this->server->expects($this->any())
->method('getRequestUri')
- ->will($this->returnValue($path));
+ ->willReturn($path);
$this->plugin->initialize($this->server);
$this->plugin->httpPost($request, $response);
}
- /**
- * @expectedException \Sabre\DAV\Exception\BadRequest
- * @expectedExceptionMessage Message exceeds allowed character limit of
- */
- public function testCreateCommentMessageTooLong() {
+
+ public function testCreateCommentMessageTooLong(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+ $this->expectExceptionMessage('Message exceeds allowed character limit of');
+
$commentData = [
'actorType' => 'users',
'verb' => 'comment',
@@ -519,32 +501,34 @@ class CommentsPlugin extends \Test\TestCase {
];
$comment = new Comment([
- 'objectType' => 'files',
- 'objectId' => '42',
- 'actorType' => 'users',
- 'actorId' => 'alice',
- 'verb' => 'comment',
- ]);
+ 'objectType' => 'files',
+ 'objectId' => '42',
+ 'actorType' => 'users',
+ 'actorId' => 'alice',
+ 'verb' => 'comment',
+ ]);
$comment->setId('23');
$path = 'comments/files/42';
$requestData = json_encode($commentData);
- $user = $this->getMock('OCP\IUser');
+ $user = $this->getMockBuilder(IUser::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$user->expects($this->once())
->method('getUID')
- ->will($this->returnValue('alice'));
+ ->willReturn('alice');
- $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection')
+ $node = $this->getMockBuilder(EntityCollection::class)
->disableOriginalConstructor()
->getMock();
$node->expects($this->once())
->method('getName')
- ->will($this->returnValue('files'));
+ ->willReturn('files');
$node->expects($this->once())
->method('getId')
- ->will($this->returnValue('42'));
+ ->willReturn('42');
$node->expects($this->never())
->method('setReadMarker');
@@ -552,11 +536,11 @@ class CommentsPlugin extends \Test\TestCase {
$this->commentsManager->expects($this->once())
->method('create')
->with('users', 'alice', 'files', '42')
- ->will($this->returnValue($comment));
+ ->willReturn($comment);
$this->userSession->expects($this->once())
->method('getUser')
- ->will($this->returnValue($user));
+ ->willReturn($user);
// technically, this is a shortcut. Inbetween EntityTypeCollection would
// be returned, but doing it exactly right would not be really
@@ -565,88 +549,96 @@ class CommentsPlugin extends \Test\TestCase {
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->returnValue($node));
+ ->willReturn($node);
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ $request = $this->getMockBuilder(RequestInterface::class)
->disableOriginalConstructor()
->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
+ $response = $this->getMockBuilder(ResponseInterface::class)
->disableOriginalConstructor()
->getMock();
$request->expects($this->once())
->method('getPath')
- ->will($this->returnValue('/' . $path));
+ ->willReturn('/' . $path);
$request->expects($this->once())
->method('getBodyAsString')
- ->will($this->returnValue($requestData));
+ ->willReturn($requestData);
$request->expects($this->once())
->method('getHeader')
->with('Content-Type')
- ->will($this->returnValue('application/json'));
+ ->willReturn('application/json');
$response->expects($this->never())
->method('setHeader');
$this->server->expects($this->any())
->method('getRequestUri')
- ->will($this->returnValue($path));
+ ->willReturn($path);
$this->plugin->initialize($this->server);
$this->plugin->httpPost($request, $response);
}
- /**
- * @expectedException \Sabre\DAV\Exception\ReportNotSupported
- */
- public function testOnReportInvalidNode() {
+
+ public function testOnReportInvalidNode(): void {
+ $this->expectException(\Sabre\DAV\Exception\ReportNotSupported::class);
+
$path = 'totally/unrelated/13';
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->returnValue($this->getMock('\Sabre\DAV\INode')));
+ ->willReturn(
+ $this->getMockBuilder(INode::class)
+ ->disableOriginalConstructor()
+ ->getMock()
+ );
$this->server->expects($this->any())
->method('getRequestUri')
- ->will($this->returnValue($path));
+ ->willReturn($path);
$this->plugin->initialize($this->server);
$this->plugin->onReport(CommentsPluginImplementation::REPORT_NAME, [], '/' . $path);
}
- /**
- * @expectedException \Sabre\DAV\Exception\ReportNotSupported
- */
- public function testOnReportInvalidReportName() {
+
+ public function testOnReportInvalidReportName(): void {
+ $this->expectException(\Sabre\DAV\Exception\ReportNotSupported::class);
+
$path = 'comments/files/42';
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->returnValue($this->getMock('\Sabre\DAV\INode')));
+ ->willReturn(
+ $this->getMockBuilder(INode::class)
+ ->disableOriginalConstructor()
+ ->getMock()
+ );
$this->server->expects($this->any())
->method('getRequestUri')
- ->will($this->returnValue($path));
+ ->willReturn($path);
$this->plugin->initialize($this->server);
$this->plugin->onReport('{whoever}whatever', [], '/' . $path);
}
- public function testOnReportDateTimeEmpty() {
+ public function testOnReportDateTimeEmpty(): void {
$path = 'comments/files/42';
$parameters = [
[
- 'name' => '{http://owncloud.org/ns}limit',
+ 'name' => '{http://owncloud.org/ns}limit',
'value' => 5,
],
[
- 'name' => '{http://owncloud.org/ns}offset',
+ 'name' => '{http://owncloud.org/ns}offset',
'value' => 10,
],
[
@@ -655,15 +647,15 @@ class CommentsPlugin extends \Test\TestCase {
]
];
- $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection')
+ $node = $this->getMockBuilder(EntityCollection::class)
->disableOriginalConstructor()
->getMock();
$node->expects($this->once())
->method('findChildren')
->with(5, 10, null)
- ->will($this->returnValue([]));
+ ->willReturn([]);
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
+ $response = $this->getMockBuilder(ResponseInterface::class)
->disableOriginalConstructor()
->getMock();
@@ -681,27 +673,27 @@ class CommentsPlugin extends \Test\TestCase {
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->returnValue($node));
+ ->willReturn($node);
$this->server->expects($this->any())
->method('getRequestUri')
- ->will($this->returnValue($path));
+ ->willReturn($path);
$this->server->httpResponse = $response;
$this->plugin->initialize($this->server);
$this->plugin->onReport(CommentsPluginImplementation::REPORT_NAME, $parameters, '/' . $path);
}
- public function testOnReport() {
+ public function testOnReport(): void {
$path = 'comments/files/42';
$parameters = [
[
- 'name' => '{http://owncloud.org/ns}limit',
+ 'name' => '{http://owncloud.org/ns}limit',
'value' => 5,
],
[
- 'name' => '{http://owncloud.org/ns}offset',
+ 'name' => '{http://owncloud.org/ns}offset',
'value' => 10,
],
[
@@ -710,15 +702,15 @@ class CommentsPlugin extends \Test\TestCase {
]
];
- $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection')
+ $node = $this->getMockBuilder(EntityCollection::class)
->disableOriginalConstructor()
->getMock();
$node->expects($this->once())
->method('findChildren')
->with(5, 10, new \DateTime($parameters[2]['value']))
- ->will($this->returnValue([]));
+ ->willReturn([]);
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
+ $response = $this->getMockBuilder(ResponseInterface::class)
->disableOriginalConstructor()
->getMock();
@@ -736,17 +728,14 @@ class CommentsPlugin extends \Test\TestCase {
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->returnValue($node));
+ ->willReturn($node);
$this->server->expects($this->any())
->method('getRequestUri')
- ->will($this->returnValue($path));
+ ->willReturn($path);
$this->server->httpResponse = $response;
$this->plugin->initialize($this->server);
$this->plugin->onReport(CommentsPluginImplementation::REPORT_NAME, $parameters, '/' . $path);
}
-
-
-
}
diff --git a/apps/dav/tests/unit/Comments/EntityCollectionTest.php b/apps/dav/tests/unit/Comments/EntityCollectionTest.php
new file mode 100644
index 00000000000..29ebde7d602
--- /dev/null
+++ b/apps/dav/tests/unit/Comments/EntityCollectionTest.php
@@ -0,0 +1,121 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Comments;
+
+use OCA\DAV\Comments\CommentNode;
+use OCA\DAV\Comments\EntityCollection;
+use OCP\Comments\IComment;
+use OCP\Comments\ICommentsManager;
+use OCP\Comments\NotFoundException;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+class EntityCollectionTest extends \Test\TestCase {
+ protected ICommentsManager&MockObject $commentsManager;
+ protected IUserManager&MockObject $userManager;
+ protected LoggerInterface&MockObject $logger;
+ protected IUserSession&MockObject $userSession;
+ protected EntityCollection $collection;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->collection = new EntityCollection(
+ '19',
+ 'files',
+ $this->commentsManager,
+ $this->userManager,
+ $this->userSession,
+ $this->logger
+ );
+ }
+
+ public function testGetId(): void {
+ $this->assertSame($this->collection->getId(), '19');
+ }
+
+ public function testGetChild(): void {
+ $this->commentsManager->expects($this->once())
+ ->method('get')
+ ->with('55')
+ ->willReturn(
+ $this->getMockBuilder(IComment::class)
+ ->disableOriginalConstructor()
+ ->getMock()
+ );
+
+ $node = $this->collection->getChild('55');
+ $this->assertInstanceOf(CommentNode::class, $node);
+ }
+
+
+ public function testGetChildException(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $this->commentsManager->expects($this->once())
+ ->method('get')
+ ->with('55')
+ ->willThrowException(new NotFoundException());
+
+ $this->collection->getChild('55');
+ }
+
+ public function testGetChildren(): void {
+ $this->commentsManager->expects($this->once())
+ ->method('getForObject')
+ ->with('files', '19')
+ ->willReturn([
+ $this->getMockBuilder(IComment::class)
+ ->disableOriginalConstructor()
+ ->getMock()
+ ]);
+
+ $result = $this->collection->getChildren();
+
+ $this->assertCount(1, $result);
+ $this->assertInstanceOf(CommentNode::class, $result[0]);
+ }
+
+ public function testFindChildren(): void {
+ $dt = new \DateTime('2016-01-10 18:48:00');
+ $this->commentsManager->expects($this->once())
+ ->method('getForObject')
+ ->with('files', '19', 5, 15, $dt)
+ ->willReturn([
+ $this->getMockBuilder(IComment::class)
+ ->disableOriginalConstructor()
+ ->getMock()
+ ]);
+
+ $result = $this->collection->findChildren(5, 15, $dt);
+
+ $this->assertCount(1, $result);
+ $this->assertInstanceOf(CommentNode::class, $result[0]);
+ }
+
+ public function testChildExistsTrue(): void {
+ $this->assertTrue($this->collection->childExists('44'));
+ }
+
+ public function testChildExistsFalse(): void {
+ $this->commentsManager->expects($this->once())
+ ->method('get')
+ ->with('44')
+ ->willThrowException(new NotFoundException());
+
+ $this->assertFalse($this->collection->childExists('44'));
+ }
+}
diff --git a/apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php b/apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php
new file mode 100644
index 00000000000..e5178a3e786
--- /dev/null
+++ b/apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php
@@ -0,0 +1,77 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Comments;
+
+use OCA\DAV\Comments\EntityCollection as EntityCollectionImplemantation;
+use OCA\DAV\Comments\EntityTypeCollection;
+use OCP\Comments\ICommentsManager;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+class EntityTypeCollectionTest extends \Test\TestCase {
+ protected ICommentsManager&MockObject $commentsManager;
+ protected IUserManager&MockObject $userManager;
+ protected LoggerInterface&MockObject $logger;
+ protected IUserSession&MockObject $userSession;
+ protected EntityTypeCollection $collection;
+
+ protected $childMap = [];
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->collection = new EntityTypeCollection(
+ 'files',
+ $this->commentsManager,
+ $this->userManager,
+ $this->userSession,
+ $this->logger,
+ function ($child) {
+ return !empty($this->childMap[$child]);
+ }
+ );
+ }
+
+ public function testChildExistsYes(): void {
+ $this->childMap[17] = true;
+ $this->assertTrue($this->collection->childExists('17'));
+ }
+
+ public function testChildExistsNo(): void {
+ $this->assertFalse($this->collection->childExists('17'));
+ }
+
+ public function testGetChild(): void {
+ $this->childMap[17] = true;
+
+ $ec = $this->collection->getChild('17');
+ $this->assertInstanceOf(EntityCollectionImplemantation::class, $ec);
+ }
+
+
+ public function testGetChildException(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $this->collection->getChild('17');
+ }
+
+
+ public function testGetChildren(): void {
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+
+ $this->collection->getChildren();
+ }
+}
diff --git a/apps/dav/tests/unit/Comments/RootCollectionTest.php b/apps/dav/tests/unit/Comments/RootCollectionTest.php
new file mode 100644
index 00000000000..9a05d996c8c
--- /dev/null
+++ b/apps/dav/tests/unit/Comments/RootCollectionTest.php
@@ -0,0 +1,161 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Comments;
+
+use OC\EventDispatcher\EventDispatcher;
+use OCA\DAV\Comments\EntityTypeCollection as EntityTypeCollectionImplementation;
+use OCA\DAV\Comments\RootCollection;
+use OCP\Comments\CommentsEntityEvent;
+use OCP\Comments\ICommentsManager;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+class RootCollectionTest extends \Test\TestCase {
+ protected ICommentsManager&MockObject $commentsManager;
+ protected IUserManager&MockObject $userManager;
+ protected LoggerInterface&MockObject $logger;
+ protected IUserSession&MockObject $userSession;
+ protected IEventDispatcher $dispatcher;
+ protected IUser&MockObject $user;
+ protected RootCollection $collection;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->user = $this->createMock(IUser::class);
+
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->dispatcher = new EventDispatcher(
+ new \Symfony\Component\EventDispatcher\EventDispatcher(),
+ \OC::$server,
+ $this->logger
+ );
+
+ $this->collection = new RootCollection(
+ $this->commentsManager,
+ $this->userManager,
+ $this->userSession,
+ $this->dispatcher,
+ $this->logger
+ );
+ }
+
+ protected function prepareForInitCollections(): void {
+ $this->user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('alice');
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->user);
+
+ $this->dispatcher->addListener(CommentsEntityEvent::class, function (CommentsEntityEvent $event): void {
+ $event->addEntityCollection('files', function () {
+ return true;
+ });
+ });
+ }
+
+
+ public function testCreateFile(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->collection->createFile('foo');
+ }
+
+
+ public function testCreateDirectory(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->collection->createDirectory('foo');
+ }
+
+ public function testGetChild(): void {
+ $this->prepareForInitCollections();
+ $etc = $this->collection->getChild('files');
+ $this->assertInstanceOf(EntityTypeCollectionImplementation::class, $etc);
+ }
+
+
+ public function testGetChildInvalid(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $this->prepareForInitCollections();
+ $this->collection->getChild('robots');
+ }
+
+
+ public function testGetChildNoAuth(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
+
+ $this->collection->getChild('files');
+ }
+
+ public function testGetChildren(): void {
+ $this->prepareForInitCollections();
+ $children = $this->collection->getChildren();
+ $this->assertFalse(empty($children));
+ foreach ($children as $child) {
+ $this->assertInstanceOf(EntityTypeCollectionImplementation::class, $child);
+ }
+ }
+
+
+ public function testGetChildrenNoAuth(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
+
+ $this->collection->getChildren();
+ }
+
+ public function testChildExistsYes(): void {
+ $this->prepareForInitCollections();
+ $this->assertTrue($this->collection->childExists('files'));
+ }
+
+ public function testChildExistsNo(): void {
+ $this->prepareForInitCollections();
+ $this->assertFalse($this->collection->childExists('robots'));
+ }
+
+
+ public function testChildExistsNoAuth(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
+
+ $this->collection->childExists('files');
+ }
+
+
+ public function testDelete(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->collection->delete();
+ }
+
+ public function testGetName(): void {
+ $this->assertSame('comments', $this->collection->getName());
+ }
+
+
+ public function testSetName(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->collection->setName('foobar');
+ }
+
+ public function testGetLastModified(): void {
+ $this->assertSame(null, $this->collection->getLastModified());
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php b/apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php
new file mode 100644
index 00000000000..8b8c775c8ec
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php
@@ -0,0 +1,230 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector;
+
+use OCA\DAV\Connector\LegacyPublicAuth;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Share\Exceptions\ShareNotFound;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
+
+/**
+ * Class LegacyPublicAuthTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\Connector
+ */
+class LegacyPublicAuthTest extends \Test\TestCase {
+ private ISession&MockObject $session;
+ private IRequest&MockObject $request;
+ private IManager&MockObject $shareManager;
+ private IThrottler&MockObject $throttler;
+ private LegacyPublicAuth $auth;
+ private string|false $oldUser;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->session = $this->createMock(ISession::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->shareManager = $this->createMock(IManager::class);
+ $this->throttler = $this->createMock(IThrottler::class);
+
+ $this->auth = new LegacyPublicAuth(
+ $this->request,
+ $this->shareManager,
+ $this->session,
+ $this->throttler
+ );
+
+ // Store current user
+ $this->oldUser = \OC_User::getUser();
+ }
+
+ protected function tearDown(): void {
+ \OC_User::setIncognitoMode(false);
+
+ // Set old user
+ \OC_User::setUserId($this->oldUser);
+ if ($this->oldUser !== false) {
+ \OC_Util::setupFS($this->oldUser);
+ }
+
+ parent::tearDown();
+ }
+
+ public function testNoShare(): void {
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->willThrowException(new ShareNotFound());
+
+ $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+
+ public function testShareNoPassword(): void {
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn(null);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->willReturn($share);
+
+ $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordFancyShareType(): void {
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(42);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->willReturn($share);
+
+ $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+
+
+ public function testSharePasswordRemote(): void {
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_REMOTE);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->willReturn($share);
+
+ $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordLinkValidPassword(): void {
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_LINK);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(true);
+
+ $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordMailValidPassword(): void {
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(true);
+
+ $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testInvalidSharePasswordLinkValidSession(): void {
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_LINK);
+ $share->method('getId')->willReturn('42');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->willReturn($share);
+
+ $this->shareManager->method('checkPassword')
+ ->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(false);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('42');
+
+ $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordLinkInvalidSession(): void {
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_LINK);
+ $share->method('getId')->willReturn('42');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->willReturn($share);
+
+ $this->shareManager->method('checkPassword')
+ ->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(false);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('43');
+
+ $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+
+
+ public function testSharePasswordMailInvalidSession(): void {
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL);
+ $share->method('getId')->willReturn('42');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->willReturn($share);
+
+ $this->shareManager->method('checkPassword')
+ ->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(false);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('43');
+
+ $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/AuthTest.php b/apps/dav/tests/unit/Connector/Sabre/AuthTest.php
new file mode 100644
index 00000000000..4b42a815708
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/AuthTest.php
@@ -0,0 +1,608 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
+use OC\Authentication\TwoFactorAuth\Manager;
+use OC\User\Session;
+use OCA\DAV\Connector\Sabre\Auth;
+use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IUser;
+use OCP\Security\Bruteforce\IThrottler;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Server;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+/**
+ * Class AuthTest
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre
+ * @group DB
+ */
+class AuthTest extends TestCase {
+ private ISession&MockObject $session;
+ private Session&MockObject $userSession;
+ private IRequest&MockObject $request;
+ private Manager&MockObject $twoFactorManager;
+ private IThrottler&MockObject $throttler;
+ private Auth $auth;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->session = $this->createMock(ISession::class);
+ $this->userSession = $this->createMock(Session::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->twoFactorManager = $this->createMock(Manager::class);
+ $this->throttler = $this->createMock(IThrottler::class);
+ $this->auth = new Auth(
+ $this->session,
+ $this->userSession,
+ $this->request,
+ $this->twoFactorManager,
+ $this->throttler
+ );
+ }
+
+ public function testIsDavAuthenticatedWithoutDavSession(): void {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn(null);
+
+ $this->assertFalse(self::invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
+ }
+
+ public function testIsDavAuthenticatedWithWrongDavSession(): void {
+ $this->session
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn('AnotherUser');
+
+ $this->assertFalse(self::invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
+ }
+
+ public function testIsDavAuthenticatedWithCorrectDavSession(): void {
+ $this->session
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn('MyTestUser');
+
+ $this->assertTrue(self::invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
+ }
+
+ public function testValidateUserPassOfAlreadyDAVAuthenticatedUser(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->exactly(1))
+ ->method('getUID')
+ ->willReturn('MyTestUser');
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->userSession
+ ->expects($this->exactly(1))
+ ->method('getUser')
+ ->willReturn($user);
+ $this->session
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn('MyTestUser');
+ $this->session
+ ->expects($this->once())
+ ->method('close');
+
+ $this->assertTrue(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
+ }
+
+ public function testValidateUserPassOfInvalidDAVAuthenticatedUser(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())
+ ->method('getUID')
+ ->willReturn('MyTestUser');
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->session
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn('AnotherUser');
+ $this->session
+ ->expects($this->once())
+ ->method('close');
+
+ $this->assertFalse(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
+ }
+
+ public function testValidateUserPassOfInvalidDAVAuthenticatedUserWithValidPassword(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->exactly(2))
+ ->method('getUID')
+ ->willReturn('MyTestUser');
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->userSession
+ ->expects($this->exactly(2))
+ ->method('getUser')
+ ->willReturn($user);
+ $this->session
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn('AnotherUser');
+ $this->userSession
+ ->expects($this->once())
+ ->method('logClientIn')
+ ->with('MyTestUser', 'MyTestPassword', $this->request)
+ ->willReturn(true);
+ $this->session
+ ->expects($this->once())
+ ->method('set')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND', 'MyTestUser');
+ $this->session
+ ->expects($this->once())
+ ->method('close');
+
+ $this->assertTrue(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
+ }
+
+ public function testValidateUserPassWithInvalidPassword(): void {
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $this->userSession
+ ->expects($this->once())
+ ->method('logClientIn')
+ ->with('MyTestUser', 'MyTestPassword')
+ ->willReturn(false);
+ $this->session
+ ->expects($this->once())
+ ->method('close');
+
+ $this->assertFalse(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
+ }
+
+
+ public function testValidateUserPassWithPasswordLoginForbidden(): void {
+ $this->expectException(PasswordLoginForbidden::class);
+
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $this->userSession
+ ->expects($this->once())
+ ->method('logClientIn')
+ ->with('MyTestUser', 'MyTestPassword')
+ ->willThrowException(new PasswordLoginForbiddenException());
+ $this->session
+ ->expects($this->once())
+ ->method('close');
+
+ self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']);
+ }
+
+ public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGet(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->request
+ ->expects($this->any())
+ ->method('getMethod')
+ ->willReturn('POST');
+ $this->session
+ ->expects($this->any())
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn(null);
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('MyWrongDavUser');
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+
+ $expectedResponse = [
+ false,
+ "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured",
+ ];
+ $response = $this->auth->check($request, $response);
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndCorrectlyDavAuthenticated(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->request
+ ->expects($this->any())
+ ->method('getMethod')
+ ->willReturn('PROPFIND');
+ $this->request
+ ->expects($this->any())
+ ->method('isUserAgent')
+ ->willReturn(false);
+ $this->session
+ ->expects($this->any())
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn('LoggedInUser');
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('LoggedInUser');
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+ $this->auth->check($request, $response);
+ }
+
+
+ public function testAuthenticateAlreadyLoggedInWithoutTwoFactorChallengePassed(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
+ $this->expectExceptionMessage('2FA challenge not passed.');
+
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->request
+ ->expects($this->any())
+ ->method('getMethod')
+ ->willReturn('PROPFIND');
+ $this->request
+ ->expects($this->any())
+ ->method('isUserAgent')
+ ->willReturn(false);
+ $this->session
+ ->expects($this->any())
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn('LoggedInUser');
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('LoggedInUser');
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+ $this->twoFactorManager->expects($this->once())
+ ->method('needsSecondFactor')
+ ->with($user)
+ ->willReturn(true);
+ $this->auth->check($request, $response);
+ }
+
+
+ public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndIncorrectlyDavAuthenticated(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
+ $this->expectExceptionMessage('CSRF check not passed.');
+
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->request
+ ->expects($this->any())
+ ->method('getMethod')
+ ->willReturn('PROPFIND');
+ $this->request
+ ->expects($this->any())
+ ->method('isUserAgent')
+ ->willReturn(false);
+ $this->session
+ ->expects($this->any())
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn('AnotherUser');
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('LoggedInUser');
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+ $this->auth->check($request, $response);
+ }
+
+ public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGetAndDesktopClient(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->request
+ ->expects($this->any())
+ ->method('getMethod')
+ ->willReturn('POST');
+ $this->request
+ ->expects($this->any())
+ ->method('isUserAgent')
+ ->willReturn(true);
+ $this->session
+ ->expects($this->any())
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn(null);
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('MyWrongDavUser');
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+
+ $this->auth->check($request, $response);
+ }
+
+ public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForGet(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->session
+ ->expects($this->any())
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn(null);
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('MyWrongDavUser');
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->request
+ ->expects($this->any())
+ ->method('getMethod')
+ ->willReturn('GET');
+
+ $response = $this->auth->check($request, $response);
+ $this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response);
+ }
+
+ public function testAuthenticateAlreadyLoggedInWithCsrfTokenForGet(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->session
+ ->expects($this->any())
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn(null);
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('MyWrongDavUser');
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+
+ $response = $this->auth->check($request, $response);
+ $this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response);
+ }
+
+ public function testAuthenticateNoBasicAuthenticateHeadersProvided(): void {
+ $server = $this->createMock(Server::class);
+ $server->httpRequest = $this->createMock(RequestInterface::class);
+ $server->httpResponse = $this->createMock(ResponseInterface::class);
+ $response = $this->auth->check($server->httpRequest, $server->httpResponse);
+ $this->assertEquals([false, 'No \'Authorization: Basic\' header found. Either the client didn\'t send one, or the server is misconfigured'], $response);
+ }
+
+
+ public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjax(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
+ $this->expectExceptionMessage('Cannot authenticate over ajax calls');
+
+ /** @var \Sabre\HTTP\RequestInterface&MockObject $httpRequest */
+ $httpRequest = $this->createMock(RequestInterface::class);
+ /** @var \Sabre\HTTP\ResponseInterface&MockObject $httpResponse */
+ $httpResponse = $this->createMock(ResponseInterface::class);
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $httpRequest
+ ->expects($this->exactly(2))
+ ->method('getHeader')
+ ->willReturnMap([
+ ['X-Requested-With', 'XMLHttpRequest'],
+ ['Authorization', null],
+ ]);
+
+ $this->auth->check($httpRequest, $httpResponse);
+ }
+
+ public function testAuthenticateWithBasicAuthenticateHeadersProvidedWithAjax(): void {
+ // No CSRF
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+
+ /** @var \Sabre\HTTP\RequestInterface&MockObject $httpRequest */
+ $httpRequest = $this->createMock(RequestInterface::class);
+ /** @var \Sabre\HTTP\ResponseInterface&MockObject $httpResponse */
+ $httpResponse = $this->createMock(ResponseInterface::class);
+ $httpRequest
+ ->expects($this->any())
+ ->method('getHeader')
+ ->willReturnMap([
+ ['X-Requested-With', 'XMLHttpRequest'],
+ ['Authorization', 'basic dXNlcm5hbWU6cGFzc3dvcmQ='],
+ ]);
+
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('MyDavUser');
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $this->userSession
+ ->expects($this->once())
+ ->method('logClientIn')
+ ->with('username', 'password')
+ ->willReturn(true);
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->auth->check($httpRequest, $httpResponse);
+ }
+
+ public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjaxButUserIsStillLoggedIn(): void {
+ /** @var \Sabre\HTTP\RequestInterface $httpRequest */
+ $httpRequest = $this->createMock(RequestInterface::class);
+ /** @var \Sabre\HTTP\ResponseInterface $httpResponse */
+ $httpResponse = $this->createMock(ResponseInterface::class);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('MyTestUser');
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->session
+ ->expects($this->atLeastOnce())
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->willReturn('MyTestUser');
+ $this->request
+ ->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('GET');
+ $httpRequest
+ ->expects($this->atLeastOnce())
+ ->method('getHeader')
+ ->with('Authorization')
+ ->willReturn(null);
+ $this->assertEquals(
+ [true, 'principals/users/MyTestUser'],
+ $this->auth->check($httpRequest, $httpResponse)
+ );
+ }
+
+ public function testAuthenticateValidCredentials(): void {
+ $server = $this->createMock(Server::class);
+ $server->httpRequest = $this->createMock(RequestInterface::class);
+ $server->httpRequest
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('Authorization')
+ ->willReturn('basic dXNlcm5hbWU6cGFzc3dvcmQ=');
+
+ $server->httpResponse = $this->createMock(ResponseInterface::class);
+ $this->userSession
+ ->expects($this->once())
+ ->method('logClientIn')
+ ->with('username', 'password')
+ ->willReturn(true);
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->exactly(2))
+ ->method('getUID')
+ ->willReturn('MyTestUser');
+ $this->userSession
+ ->expects($this->exactly(3))
+ ->method('getUser')
+ ->willReturn($user);
+ $response = $this->auth->check($server->httpRequest, $server->httpResponse);
+ $this->assertEquals([true, 'principals/users/MyTestUser'], $response);
+ }
+
+ public function testAuthenticateInvalidCredentials(): void {
+ $server = $this->createMock(Server::class);
+ $server->httpRequest = $this->createMock(RequestInterface::class);
+ $server->httpRequest
+ ->expects($this->exactly(2))
+ ->method('getHeader')
+ ->willReturnMap([
+ ['Authorization', 'basic dXNlcm5hbWU6cGFzc3dvcmQ='],
+ ['X-Requested-With', null],
+ ]);
+ $server->httpResponse = $this->createMock(ResponseInterface::class);
+ $this->userSession
+ ->expects($this->once())
+ ->method('logClientIn')
+ ->with('username', 'password')
+ ->willReturn(false);
+ $response = $this->auth->check($server->httpRequest, $server->httpResponse);
+ $this->assertEquals([false, 'Username or password was incorrect'], $response);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
new file mode 100644
index 00000000000..1e6267d4cbb
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\User\Session;
+use OCA\DAV\Connector\Sabre\BearerAuth;
+use OCP\IConfig;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class BearerAuthTest extends TestCase {
+ private IUserSession&MockObject $userSession;
+ private ISession&MockObject $session;
+ private IRequest&MockObject $request;
+ private BearerAuth $bearerAuth;
+
+ private IConfig&MockObject $config;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userSession = $this->createMock(Session::class);
+ $this->session = $this->createMock(ISession::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $this->bearerAuth = new BearerAuth(
+ $this->userSession,
+ $this->session,
+ $this->request,
+ $this->config,
+ );
+ }
+
+ public function testValidateBearerTokenNotLoggedIn(): void {
+ $this->assertFalse($this->bearerAuth->validateBearerToken('Token'));
+ }
+
+ public function testValidateBearerToken(): void {
+ $this->userSession
+ ->expects($this->exactly(2))
+ ->method('isLoggedIn')
+ ->willReturnOnConsecutiveCalls(
+ false,
+ true,
+ );
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('admin');
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->assertSame('principals/users/admin', $this->bearerAuth->validateBearerToken('Token'));
+ }
+
+ public function testChallenge(): void {
+ /** @var RequestInterface&MockObject $request */
+ $request = $this->createMock(RequestInterface::class);
+ /** @var ResponseInterface&MockObject $response */
+ $response = $this->createMock(ResponseInterface::class);
+ $result = $this->bearerAuth->challenge($request, $response);
+ $this->assertEmpty($result);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php
new file mode 100644
index 00000000000..366c9475b1b
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php
@@ -0,0 +1,177 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
+use OCA\Theming\ThemingDefaults;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\HTTP\RequestInterface;
+use Test\TestCase;
+
+enum ERROR_TYPE {
+ case MIN_ERROR;
+ case MAX_ERROR;
+ case NONE;
+}
+
+/**
+ * Class BlockLegacyClientPluginTest
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre
+ */
+class BlockLegacyClientPluginTest extends TestCase {
+
+ private IConfig&MockObject $config;
+ private ThemingDefaults&MockObject $themingDefaults;
+ private BlockLegacyClientPlugin $blockLegacyClientVersionPlugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->themingDefaults = $this->createMock(ThemingDefaults::class);
+ $this->blockLegacyClientVersionPlugin = new BlockLegacyClientPlugin(
+ $this->config,
+ $this->themingDefaults,
+ );
+ }
+
+ public static function oldDesktopClientProvider(): array {
+ return [
+ ['Mozilla/5.0 (Windows) mirall/1.5.0', ERROR_TYPE::MIN_ERROR],
+ ['Mozilla/5.0 (Bogus Text) mirall/1.6.9', ERROR_TYPE::MIN_ERROR],
+ ['Mozilla/5.0 (Windows) mirall/2.5.0', ERROR_TYPE::MAX_ERROR],
+ ['Mozilla/5.0 (Bogus Text) mirall/2.0.1', ERROR_TYPE::MAX_ERROR],
+ ['Mozilla/5.0 (Windows) mirall/2.0.0', ERROR_TYPE::NONE],
+ ['Mozilla/5.0 (Bogus Text) mirall/2.0.0', ERROR_TYPE::NONE],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('oldDesktopClientProvider')]
+ public function testBeforeHandlerException(string $userAgent, ERROR_TYPE $errorType): void {
+ $this->themingDefaults
+ ->expects($this->atMost(1))
+ ->method('getSyncClientUrl')
+ ->willReturn('https://nextcloud.com/install/#install-clients');
+
+ $this->config
+ ->expects($this->exactly(2))
+ ->method('getSystemValueString')
+ ->willReturnCallback(function (string $key) {
+ if ($key === 'minimum.supported.desktop.version') {
+ return '1.7.0';
+ }
+ return '2.0.0';
+ });
+
+ if ($errorType !== ERROR_TYPE::NONE) {
+ $errorString = $errorType === ERROR_TYPE::MIN_ERROR
+ ? 'This version of the client is unsupported. Upgrade to <a href="https://nextcloud.com/install/#install-clients">version 1.7.0 or later</a>.'
+ : 'This version of the client is unsupported. Downgrade to <a href="https://nextcloud.com/install/#install-clients">version 2.0.0 or earlier</a>.';
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage($errorString);
+ }
+
+ /** @var RequestInterface|MockObject $request */
+ $request = $this->createMock(RequestInterface::class);
+ $request
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('User-Agent')
+ ->willReturn($userAgent);
+
+ $this->blockLegacyClientVersionPlugin->beforeHandler($request);
+ }
+
+ /**
+ * Ensure that there is no room for XSS attack through configured URL / version
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('oldDesktopClientProvider')]
+ public function testBeforeHandlerExceptionPreventXSSAttack(string $userAgent, ERROR_TYPE $errorType): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->themingDefaults
+ ->expects($this->atMost(1))
+ ->method('getSyncClientUrl')
+ ->willReturn('https://example.com"><script>alter("hacked");</script>');
+
+ $this->config
+ ->expects($this->exactly(2))
+ ->method('getSystemValueString')
+ ->willReturnCallback(function (string $key) {
+ if ($key === 'minimum.supported.desktop.version') {
+ return '1.7.0 <script>alert("unsafe")</script>';
+ }
+ return '2.0.0 <script>alert("unsafe")</script>';
+ });
+
+ $errorString = $errorType === ERROR_TYPE::MIN_ERROR
+ ? 'This version of the client is unsupported. Upgrade to <a href="https://example.com&quot;&gt;&lt;script&gt;alter(&quot;hacked&quot;);&lt;/script&gt;">version 1.7.0 &lt;script&gt;alert(&quot;unsafe&quot;)&lt;/script&gt; or later</a>.'
+ : 'This version of the client is unsupported. Downgrade to <a href="https://example.com&quot;&gt;&lt;script&gt;alter(&quot;hacked&quot;);&lt;/script&gt;">version 2.0.0 &lt;script&gt;alert(&quot;unsafe&quot;)&lt;/script&gt; or earlier</a>.';
+ $this->expectExceptionMessage($errorString);
+
+ /** @var RequestInterface|MockObject $request */
+ $request = $this->createMock('\Sabre\HTTP\RequestInterface');
+ $request
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('User-Agent')
+ ->willReturn($userAgent);
+
+ $this->blockLegacyClientVersionPlugin->beforeHandler($request);
+ }
+
+ public static function newAndAlternateDesktopClientProvider(): array {
+ return [
+ ['Mozilla/5.0 (Windows) mirall/1.7.0'],
+ ['Mozilla/5.0 (Bogus Text) mirall/1.9.3'],
+ ['Mozilla/5.0 (Not Our Client But Old Version) LegacySync/1.1.0'],
+ ['Mozilla/5.0 (Windows) mirall/4.7.0'],
+ ['Mozilla/5.0 (Bogus Text) mirall/3.9.3'],
+ ['Mozilla/5.0 (Not Our Client But Old Version) LegacySync/45.0.0'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('newAndAlternateDesktopClientProvider')]
+ public function testBeforeHandlerSuccess(string $userAgent): void {
+ /** @var RequestInterface|MockObject $request */
+ $request = $this->createMock(RequestInterface::class);
+ $request
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('User-Agent')
+ ->willReturn($userAgent);
+
+ $this->config
+ ->expects($this->exactly(2))
+ ->method('getSystemValueString')
+ ->willReturnCallback(function (string $key) {
+ if ($key === 'minimum.supported.desktop.version') {
+ return '1.7.0';
+ }
+ return '10.0.0';
+ });
+
+ $this->blockLegacyClientVersionPlugin->beforeHandler($request);
+ }
+
+ public function testBeforeHandlerNoUserAgent(): void {
+ /** @var RequestInterface|MockObject $request */
+ $request = $this->createMock(RequestInterface::class);
+ $request
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('User-Agent')
+ ->willReturn(null);
+
+ $this->blockLegacyClientVersionPlugin->beforeHandler($request);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php
new file mode 100644
index 00000000000..a934d6401c2
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php
@@ -0,0 +1,117 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\CommentPropertiesPlugin as CommentPropertiesPluginImplementation;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\File;
+use OCP\Comments\ICommentsManager;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
+
+class CommentsPropertiesPluginTest extends \Test\TestCase {
+ protected CommentPropertiesPluginImplementation $plugin;
+ protected ICommentsManager&MockObject $commentsManager;
+ protected IUserSession&MockObject $userSession;
+ protected Server&MockObject $server;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->server = $this->createMock(Server::class);
+
+ $this->plugin = new CommentPropertiesPluginImplementation($this->commentsManager, $this->userSession);
+ $this->plugin->initialize($this->server);
+ }
+
+ public static function nodeProvider(): array {
+ return [
+ [File::class, true],
+ [Directory::class, true],
+ [\Sabre\DAV\INode::class, false]
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('nodeProvider')]
+ public function testHandleGetProperties(string $class, bool $expectedSuccessful): void {
+ $propFind = $this->createMock(PropFind::class);
+
+ if ($expectedSuccessful) {
+ $propFind->expects($this->exactly(3))
+ ->method('handle');
+ } else {
+ $propFind->expects($this->never())
+ ->method('handle');
+ }
+
+ $node = $this->createMock($class);
+ $this->plugin->handleGetProperties($propFind, $node);
+ }
+
+ public static function baseUriProvider(): array {
+ return [
+ ['owncloud/remote.php/webdav/', '4567', 'owncloud/remote.php/dav/comments/files/4567'],
+ ['owncloud/remote.php/files/', '4567', 'owncloud/remote.php/dav/comments/files/4567'],
+ ['owncloud/wicked.php/files/', '4567', null]
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('baseUriProvider')]
+ public function testGetCommentsLink(string $baseUri, string $fid, ?string $expectedHref): void {
+ $node = $this->createMock(File::class);
+ $node->expects($this->any())
+ ->method('getId')
+ ->willReturn($fid);
+
+ $this->server->expects($this->once())
+ ->method('getBaseUri')
+ ->willReturn($baseUri);
+
+ $href = $this->plugin->getCommentsLink($node);
+ $this->assertSame($expectedHref, $href);
+ }
+
+ public static function userProvider(): array {
+ return [
+ [IUser::class],
+ [null]
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('userProvider')]
+ public function testGetUnreadCount(?string $user): void {
+ $node = $this->createMock(File::class);
+ $node->expects($this->any())
+ ->method('getId')
+ ->willReturn('4567');
+
+ if ($user !== null) {
+ $user = $this->createMock($user);
+ }
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->commentsManager->expects($this->any())
+ ->method('getNumberOfCommentsForObject')
+ ->willReturn(42);
+
+ $unread = $this->plugin->getUnreadCount($node);
+ if (is_null($user)) {
+ $this->assertNull($unread);
+ } else {
+ $this->assertSame($unread, 42);
+ }
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php
new file mode 100644
index 00000000000..7067cf335ed
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin;
+use OCA\DAV\Connector\Sabre\File;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Test\TestCase;
+
+class CopyEtagHeaderPluginTest extends TestCase {
+ private CopyEtagHeaderPlugin $plugin;
+ private Server $server;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->server = new \Sabre\DAV\Server();
+ $this->plugin = new CopyEtagHeaderPlugin();
+ $this->plugin->initialize($this->server);
+ }
+
+ public function testCopyEtag(): void {
+ $request = new \Sabre\Http\Request('GET', 'dummy.file');
+ $response = new \Sabre\Http\Response();
+ $response->setHeader('Etag', 'abcd');
+
+ $this->plugin->afterMethod($request, $response);
+
+ $this->assertEquals('abcd', $response->getHeader('OC-Etag'));
+ }
+
+ public function testNoopWhenEmpty(): void {
+ $request = new \Sabre\Http\Request('GET', 'dummy.file');
+ $response = new \Sabre\Http\Response();
+
+ $this->plugin->afterMethod($request, $response);
+
+ $this->assertNull($response->getHeader('OC-Etag'));
+ }
+
+ public function testAfterMoveNodeNotFound(): void {
+ $tree = $this->createMock(Tree::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('test.txt')
+ ->willThrowException(new NotFound());
+
+ $this->server->tree = $tree;
+ $this->plugin->afterMove('', 'test.txt');
+
+ // Nothing to assert, we are just testing if the exception is handled
+ }
+
+ public function testAfterMove(): void {
+ $node = $this->createMock(File::class);
+ $node->expects($this->once())
+ ->method('getETag')
+ ->willReturn('123456');
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('test.txt')
+ ->willReturn($node);
+
+ $this->server->tree = $tree;
+ $this->plugin->afterMove('', 'test.txt');
+
+ $this->assertEquals('123456', $this->server->httpResponse->getHeader('OC-Etag'));
+ $this->assertEquals('123456', $this->server->httpResponse->getHeader('Etag'));
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php
new file mode 100644
index 00000000000..cafbdd3ca40
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php
@@ -0,0 +1,234 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\File;
+use OCA\DAV\DAV\CustomPropertiesBackend;
+use OCA\DAV\Db\PropertyMapper;
+use OCP\IDBConnection;
+use OCP\IUser;
+use OCP\Server;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Tree;
+
+/**
+ * Class CustomPropertiesBackend
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre
+ */
+class CustomPropertiesBackendTest extends \Test\TestCase {
+ private \Sabre\DAV\Server $server;
+ private \Sabre\DAV\Tree&MockObject $tree;
+ private IUser&MockObject $user;
+ private DefaultCalendarValidator&MockObject $defaultCalendarValidator;
+ private CustomPropertiesBackend $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->server = new \Sabre\DAV\Server();
+ $this->tree = $this->createMock(Tree::class);
+
+ $userId = self::getUniqueID('testcustompropertiesuser');
+
+ $this->user = $this->createMock(IUser::class);
+ $this->user->expects($this->any())
+ ->method('getUID')
+ ->willReturn($userId);
+
+ $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class);
+
+ $this->plugin = new CustomPropertiesBackend(
+ $this->server,
+ $this->tree,
+ Server::get(IDBConnection::class),
+ $this->user,
+ Server::get(PropertyMapper::class),
+ $this->defaultCalendarValidator,
+ );
+ }
+
+ protected function tearDown(): void {
+ $connection = Server::get(IDBConnection::class);
+ $deleteStatement = $connection->prepare(
+ 'DELETE FROM `*PREFIX*properties`'
+ . ' WHERE `userid` = ?'
+ );
+ $deleteStatement->execute(
+ [
+ $this->user->getUID(),
+ ]
+ );
+ $deleteStatement->closeCursor();
+
+ parent::tearDown();
+ }
+
+ private function createTestNode(string $class) {
+ $node = $this->createMock($class);
+ $node->expects($this->any())
+ ->method('getId')
+ ->willReturn(123);
+
+ $node->expects($this->any())
+ ->method('getPath')
+ ->willReturn('/dummypath');
+
+ return $node;
+ }
+
+ private function applyDefaultProps($path = '/dummypath'): void {
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch([
+ 'customprop' => 'value1',
+ 'customprop2' => 'value2',
+ ]);
+
+ $this->plugin->propPatch(
+ $path,
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(200, $result['customprop']);
+ $this->assertEquals(200, $result['customprop2']);
+ }
+
+ /**
+ * Test that propFind on a missing file soft fails
+ */
+ public function testPropFindMissingFileSoftFail(): void {
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummypath',
+ [
+ 'customprop',
+ 'customprop2',
+ 'unsetprop',
+ ],
+ 0
+ );
+
+ $this->plugin->propFind(
+ '/dummypath',
+ $propFind
+ );
+
+ $this->plugin->propFind(
+ '/dummypath',
+ $propFind
+ );
+
+ // assert that the above didn't throw exceptions
+ $this->assertTrue(true);
+ }
+
+ /**
+ * Test setting/getting properties
+ */
+ public function testSetGetPropertiesForFile(): void {
+ $this->applyDefaultProps();
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummypath',
+ [
+ 'customprop',
+ 'customprop2',
+ 'unsetprop',
+ ],
+ 0
+ );
+
+ $this->plugin->propFind(
+ '/dummypath',
+ $propFind
+ );
+
+ $this->assertEquals('value1', $propFind->get('customprop'));
+ $this->assertEquals('value2', $propFind->get('customprop2'));
+ $this->assertEquals(['unsetprop'], $propFind->get404Properties());
+ }
+
+ /**
+ * Test getting properties from directory
+ */
+ public function testGetPropertiesForDirectory(): void {
+ $this->applyDefaultProps('/dummypath');
+ $this->applyDefaultProps('/dummypath/test.txt');
+
+ $propNames = [
+ 'customprop',
+ 'customprop2',
+ 'unsetprop',
+ ];
+
+ $propFindRoot = new \Sabre\DAV\PropFind(
+ '/dummypath',
+ $propNames,
+ 1
+ );
+
+ $propFindSub = new \Sabre\DAV\PropFind(
+ '/dummypath/test.txt',
+ $propNames,
+ 0
+ );
+
+ $this->plugin->propFind(
+ '/dummypath',
+ $propFindRoot
+ );
+
+ $this->plugin->propFind(
+ '/dummypath/test.txt',
+ $propFindSub
+ );
+
+ // TODO: find a way to assert that no additional SQL queries were
+ // run while doing the second propFind
+
+ $this->assertEquals('value1', $propFindRoot->get('customprop'));
+ $this->assertEquals('value2', $propFindRoot->get('customprop2'));
+ $this->assertEquals(['unsetprop'], $propFindRoot->get404Properties());
+
+ $this->assertEquals('value1', $propFindSub->get('customprop'));
+ $this->assertEquals('value2', $propFindSub->get('customprop2'));
+ $this->assertEquals(['unsetprop'], $propFindSub->get404Properties());
+ }
+
+ /**
+ * Test delete property
+ */
+ public function testDeleteProperty(): void {
+ $this->applyDefaultProps();
+
+ $propPatch = new \Sabre\DAV\PropPatch([
+ 'customprop' => null,
+ ]);
+
+ $this->plugin->propPatch(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(204, $result['customprop']);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php
new file mode 100644
index 00000000000..421ee1bdc12
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php
@@ -0,0 +1,468 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\Files\FileInfo;
+use OC\Files\Filesystem;
+use OC\Files\Node\Node;
+use OC\Files\Storage\Wrapper\Quota;
+use OC\Files\View;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\Files_Sharing\External\Storage;
+use OCP\Constants;
+use OCP\Files\ForbiddenException;
+use OCP\Files\InvalidPathException;
+use OCP\Files\Mount\IMountPoint;
+use OCP\Files\StorageNotAvailableException;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\Traits\UserTrait;
+
+class TestViewDirectory extends View {
+ public function __construct(
+ private $updatables,
+ private $deletables,
+ private $canRename = true,
+ ) {
+ }
+
+ public function isUpdatable($path) {
+ return $this->updatables[$path];
+ }
+
+ public function isCreatable($path) {
+ return $this->updatables[$path];
+ }
+
+ public function isDeletable($path) {
+ return $this->deletables[$path];
+ }
+
+ public function rename($source, $target, array $options = []) {
+ return $this->canRename;
+ }
+
+ public function getRelativePath($path): ?string {
+ return $path;
+ }
+}
+
+
+/**
+ * @group DB
+ */
+class DirectoryTest extends \Test\TestCase {
+ use UserTrait;
+
+ private View&MockObject $view;
+ private FileInfo&MockObject $info;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->view = $this->createMock(View::class);
+ $this->info = $this->createMock(FileInfo::class);
+ $this->info->method('isReadable')
+ ->willReturn(true);
+ $this->info->method('getType')
+ ->willReturn(Node::TYPE_FOLDER);
+ $this->info->method('getName')
+ ->willReturn('folder');
+ $this->info->method('getPath')
+ ->willReturn('/admin/files/folder');
+ $this->info->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_READ);
+ }
+
+ private function getDir(string $path = '/'): Directory {
+ $this->view->expects($this->once())
+ ->method('getRelativePath')
+ ->willReturn($path);
+
+ $this->info->expects($this->once())
+ ->method('getPath')
+ ->willReturn($path);
+
+ return new Directory($this->view, $this->info);
+ }
+
+
+ public function testDeleteRootFolderFails(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->info->expects($this->any())
+ ->method('isDeletable')
+ ->willReturn(true);
+ $this->view->expects($this->never())
+ ->method('rmdir');
+ $dir = $this->getDir();
+ $dir->delete();
+ }
+
+
+ public function testDeleteForbidden(): void {
+ $this->expectException(Forbidden::class);
+
+ // deletion allowed
+ $this->info->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(true);
+
+ // but fails
+ $this->view->expects($this->once())
+ ->method('rmdir')
+ ->with('sub')
+ ->willThrowException(new ForbiddenException('', true));
+
+ $dir = $this->getDir('sub');
+ $dir->delete();
+ }
+
+
+ public function testDeleteFolderWhenAllowed(): void {
+ // deletion allowed
+ $this->info->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(true);
+
+ // but fails
+ $this->view->expects($this->once())
+ ->method('rmdir')
+ ->with('sub')
+ ->willReturn(true);
+
+ $dir = $this->getDir('sub');
+ $dir->delete();
+ }
+
+
+ public function testDeleteFolderFailsWhenNotAllowed(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->info->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(false);
+
+ $dir = $this->getDir('sub');
+ $dir->delete();
+ }
+
+
+ public function testDeleteFolderThrowsWhenDeletionFailed(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ // deletion allowed
+ $this->info->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(true);
+
+ // but fails
+ $this->view->expects($this->once())
+ ->method('rmdir')
+ ->with('sub')
+ ->willReturn(false);
+
+ $dir = $this->getDir('sub');
+ $dir->delete();
+ }
+
+ public function testGetChildren(): void {
+ $info1 = $this->createMock(FileInfo::class);
+ $info2 = $this->createMock(FileInfo::class);
+ $info1->method('getName')
+ ->willReturn('first');
+ $info1->method('getPath')
+ ->willReturn('folder/first');
+ $info1->method('getEtag')
+ ->willReturn('abc');
+ $info2->method('getName')
+ ->willReturn('second');
+ $info2->method('getPath')
+ ->willReturn('folder/second');
+ $info2->method('getEtag')
+ ->willReturn('def');
+
+ $this->view->expects($this->once())
+ ->method('getDirectoryContent')
+ ->willReturn([$info1, $info2]);
+
+ $this->view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturnCallback(function ($path) {
+ return str_replace('/admin/files/', '', $path);
+ });
+
+ $this->view->expects($this->any())
+ ->method('getAbsolutePath')
+ ->willReturnCallback(function ($path) {
+ return Filesystem::normalizePath('/admin/files' . $path);
+ });
+
+ $this->overwriteService(View::class, $this->view);
+
+ $dir = new Directory($this->view, $this->info);
+ $nodes = $dir->getChildren();
+
+ $this->assertCount(2, $nodes);
+
+ // calling a second time just returns the cached values,
+ // does not call getDirectoryContents again
+ $dir->getChildren();
+ }
+
+
+ public function testGetChildrenNoPermission(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $info = $this->createMock(FileInfo::class);
+ $info->expects($this->any())
+ ->method('isReadable')
+ ->willReturn(false);
+
+ $dir = new Directory($this->view, $info);
+ $dir->getChildren();
+ }
+
+
+ public function testGetChildNoPermission(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $this->info->expects($this->any())
+ ->method('isReadable')
+ ->willReturn(false);
+
+ $dir = new Directory($this->view, $this->info);
+ $dir->getChild('test');
+ }
+
+
+ public function testGetChildThrowStorageNotAvailableException(): void {
+ $this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class);
+
+ $this->view->expects($this->once())
+ ->method('getFileInfo')
+ ->willThrowException(new StorageNotAvailableException());
+
+ $dir = new Directory($this->view, $this->info);
+ $dir->getChild('.');
+ }
+
+
+ public function testGetChildThrowInvalidPath(): void {
+ $this->expectException(InvalidPath::class);
+
+ $this->view->expects($this->once())
+ ->method('verifyPath')
+ ->willThrowException(new InvalidPathException());
+ $this->view->expects($this->never())
+ ->method('getFileInfo');
+
+ $dir = new Directory($this->view, $this->info);
+ $dir->getChild('.');
+ }
+
+ public function testGetQuotaInfoUnlimited(): void {
+ $this->createUser('user', 'password');
+ self::loginAsUser('user');
+ $mountPoint = $this->createMock(IMountPoint::class);
+ $storage = $this->createMock(Quota::class);
+ $mountPoint->method('getStorage')
+ ->willReturn($storage);
+
+ $storage->expects($this->any())
+ ->method('instanceOfStorage')
+ ->willReturnMap([
+ ['\OCA\Files_Sharing\SharedStorage', false],
+ ['\OC\Files\Storage\Wrapper\Quota', false],
+ [Storage::class, false],
+ ]);
+
+ $storage->expects($this->once())
+ ->method('getOwner')
+ ->willReturn('user');
+
+ $storage->expects($this->never())
+ ->method('getQuota');
+
+ $storage->expects($this->once())
+ ->method('free_space')
+ ->willReturn(800);
+
+ $this->info->expects($this->any())
+ ->method('getPath')
+ ->willReturn('/admin/files/foo');
+
+ $this->info->expects($this->once())
+ ->method('getSize')
+ ->willReturn(200);
+
+ $this->info->expects($this->once())
+ ->method('getMountPoint')
+ ->willReturn($mountPoint);
+
+ $this->view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturn('/foo');
+
+ $this->info->expects($this->once())
+ ->method('getInternalPath')
+ ->willReturn('/foo');
+
+ $mountPoint->method('getMountPoint')
+ ->willReturn('/user/files/mymountpoint');
+
+ $dir = new Directory($this->view, $this->info);
+ $this->assertEquals([200, -3], $dir->getQuotaInfo()); //200 used, unlimited
+ }
+
+ public function testGetQuotaInfoSpecific(): void {
+ $this->createUser('user', 'password');
+ self::loginAsUser('user');
+ $mountPoint = $this->createMock(IMountPoint::class);
+ $storage = $this->createMock(Quota::class);
+ $mountPoint->method('getStorage')
+ ->willReturn($storage);
+
+ $storage->expects($this->any())
+ ->method('instanceOfStorage')
+ ->willReturnMap([
+ ['\OCA\Files_Sharing\SharedStorage', false],
+ ['\OC\Files\Storage\Wrapper\Quota', true],
+ [Storage::class, false],
+ ]);
+
+ $storage->expects($this->once())
+ ->method('getOwner')
+ ->willReturn('user');
+
+ $storage->expects($this->once())
+ ->method('getQuota')
+ ->willReturn(1000);
+
+ $storage->expects($this->once())
+ ->method('free_space')
+ ->willReturn(800);
+
+ $this->info->expects($this->once())
+ ->method('getSize')
+ ->willReturn(200);
+
+ $this->info->expects($this->once())
+ ->method('getMountPoint')
+ ->willReturn($mountPoint);
+
+ $this->info->expects($this->once())
+ ->method('getInternalPath')
+ ->willReturn('/foo');
+
+ $mountPoint->method('getMountPoint')
+ ->willReturn('/user/files/mymountpoint');
+
+ $this->view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturn('/foo');
+
+ $dir = new Directory($this->view, $this->info);
+ $this->assertEquals([200, 800], $dir->getQuotaInfo()); //200 used, 800 free
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('moveFailedProvider')]
+ public function testMoveFailed(string $source, string $destination, array $updatables, array $deletables): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->moveTest($source, $destination, $updatables, $deletables);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('moveSuccessProvider')]
+ public function testMoveSuccess(string $source, string $destination, array $updatables, array $deletables): void {
+ $this->moveTest($source, $destination, $updatables, $deletables);
+ $this->addToAssertionCount(1);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('moveFailedInvalidCharsProvider')]
+ public function testMoveFailedInvalidChars(string $source, string $destination, array $updatables, array $deletables): void {
+ $this->expectException(InvalidPath::class);
+
+ $this->moveTest($source, $destination, $updatables, $deletables);
+ }
+
+ public static function moveFailedInvalidCharsProvider(): array {
+ return [
+ ['a/valid', "a/i\nvalid", ['a' => true, 'a/valid' => true, 'a/c*' => false], []],
+ ];
+ }
+
+ public static function moveFailedProvider(): array {
+ return [
+ ['a/b', 'a/c', ['a' => false, 'a/b' => false, 'a/c' => false], []],
+ ['a/b', 'b/b', ['a' => false, 'a/b' => false, 'b' => false, 'b/b' => false], []],
+ ['a/b', 'b/b', ['a' => false, 'a/b' => true, 'b' => false, 'b/b' => false], []],
+ ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => false, 'b/b' => false], []],
+ ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => true, 'b/b' => false], ['a/b' => false]],
+ ['a/b', 'a/c', ['a' => false, 'a/b' => true, 'a/c' => false], []],
+ ];
+ }
+
+ public static function moveSuccessProvider(): array {
+ return [
+ ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => true, 'b/b' => false], ['a/b' => true]],
+ // older files with special chars can still be renamed to valid names
+ ['a/b*', 'b/b', ['a' => true, 'a/b*' => true, 'b' => true, 'b/b' => false], ['a/b*' => true]],
+ ];
+ }
+
+ private function moveTest(string $source, string $destination, array $updatables, array $deletables): void {
+ $view = new TestViewDirectory($updatables, $deletables);
+
+ $sourceInfo = new FileInfo($source, null, null, [
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+ $targetInfo = new FileInfo(dirname($destination), null, null, [
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+
+ $sourceNode = new Directory($view, $sourceInfo);
+ $targetNode = $this->getMockBuilder(Directory::class)
+ ->onlyMethods(['childExists'])
+ ->setConstructorArgs([$view, $targetInfo])
+ ->getMock();
+ $targetNode->expects($this->any())->method('childExists')
+ ->with(basename($destination))
+ ->willReturn(false);
+ $this->assertTrue($targetNode->moveInto(basename($destination), $source, $sourceNode));
+ }
+
+
+ public function testFailingMove(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('Could not copy directory b, target exists');
+
+ $source = 'a/b';
+ $destination = 'c/b';
+ $updatables = ['a' => true, 'a/b' => true, 'b' => true, 'c/b' => false];
+ $deletables = ['a/b' => true];
+
+ $view = new TestViewDirectory($updatables, $deletables);
+
+ $sourceInfo = new FileInfo($source, null, null, ['type' => FileInfo::TYPE_FOLDER], null);
+ $targetInfo = new FileInfo(dirname($destination), null, null, ['type' => FileInfo::TYPE_FOLDER], null);
+
+ $sourceNode = new Directory($view, $sourceInfo);
+ $targetNode = $this->getMockBuilder(Directory::class)
+ ->onlyMethods(['childExists'])
+ ->setConstructorArgs([$view, $targetInfo])
+ ->getMock();
+ $targetNode->expects($this->once())->method('childExists')
+ ->with(basename($destination))
+ ->willReturn(true);
+
+ $targetNode->moveInto(basename($destination), $source, $sourceNode);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php b/apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php
new file mode 100644
index 00000000000..2d688d64600
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin;
+use Sabre\DAV\Server;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+/**
+ * Class DummyGetResponsePluginTest
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre
+ */
+class DummyGetResponsePluginTest extends TestCase {
+ private DummyGetResponsePlugin $dummyGetResponsePlugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->dummyGetResponsePlugin = new DummyGetResponsePlugin();
+ }
+
+ public function testInitialize(): void {
+ $server = $this->createMock(Server::class);
+ $server
+ ->expects($this->once())
+ ->method('on')
+ ->with('method:GET', [$this->dummyGetResponsePlugin, 'httpGet'], 200);
+
+ $this->dummyGetResponsePlugin->initialize($server);
+ }
+
+
+ public function testHttpGet(): void {
+ /** @var \Sabre\HTTP\RequestInterface $request */
+ $request = $this->createMock(RequestInterface::class);
+ /** @var \Sabre\HTTP\ResponseInterface $response */
+ $response = $this->createMock(ResponseInterface::class);
+ $response
+ ->expects($this->once())
+ ->method('setBody');
+ $response
+ ->expects($this->once())
+ ->method('setStatus')
+ ->with(200);
+
+ $this->assertSame(false, $this->dummyGetResponsePlugin->httpGet($request, $response));
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php b/apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php
new file mode 100644
index 00000000000..2f9e0ae9196
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php
@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre\Exception;
+
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use Sabre\DAV\Server;
+
+class ForbiddenTest extends \Test\TestCase {
+ public function testSerialization(): void {
+
+ // create xml doc
+ $DOM = new \DOMDocument('1.0', 'utf-8');
+ $DOM->formatOutput = true;
+ $error = $DOM->createElementNS('DAV:', 'd:error');
+ $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV);
+ $DOM->appendChild($error);
+
+ // serialize the exception
+ $message = '1234567890';
+ $retry = false;
+ $expectedXml = <<<EOD
+<?xml version="1.0" encoding="utf-8"?>
+<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns">
+ <o:retry xmlns:o="o:">false</o:retry>
+ <o:reason xmlns:o="o:">1234567890</o:reason>
+</d:error>
+
+EOD;
+
+ $ex = new Forbidden($message, $retry);
+ $server = $this->createMock(Server::class);
+ $ex->serialize($server, $error);
+
+ // assert
+ $xml = $DOM->saveXML();
+ $this->assertEquals($expectedXml, $xml);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php b/apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php
new file mode 100644
index 00000000000..6f62bef86a3
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php
@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre\Exception;
+
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use Sabre\DAV\Server;
+
+class InvalidPathTest extends \Test\TestCase {
+ public function testSerialization(): void {
+
+ // create xml doc
+ $DOM = new \DOMDocument('1.0', 'utf-8');
+ $DOM->formatOutput = true;
+ $error = $DOM->createElementNS('DAV:', 'd:error');
+ $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV);
+ $DOM->appendChild($error);
+
+ // serialize the exception
+ $message = '1234567890';
+ $retry = false;
+ $expectedXml = <<<EOD
+<?xml version="1.0" encoding="utf-8"?>
+<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns">
+ <o:retry xmlns:o="o:">false</o:retry>
+ <o:reason xmlns:o="o:">1234567890</o:reason>
+</d:error>
+
+EOD;
+
+ $ex = new InvalidPath($message, $retry);
+ $server = $this->createMock(Server::class);
+ $ex->serialize($server, $error);
+
+ // assert
+ $xml = $DOM->saveXML();
+ $this->assertEquals($expectedXml, $xml);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php
new file mode 100644
index 00000000000..416ac8a75c9
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\SystemConfig;
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
+use OCA\DAV\Exception\ServerMaintenanceMode;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Server;
+use Test\TestCase;
+
+class ExceptionLoggerPluginTest extends TestCase {
+ private Server $server;
+ private ExceptionLoggerPlugin $plugin;
+ private LoggerInterface&MockObject $logger;
+
+ private function init(): void {
+ $config = $this->createMock(SystemConfig::class);
+ $config->expects($this->any())
+ ->method('getValue')
+ ->willReturnCallback(function ($key, $default) {
+ switch ($key) {
+ case 'loglevel':
+ return 0;
+ default:
+ return $default;
+ }
+ });
+
+ $this->server = new Server();
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->plugin = new ExceptionLoggerPlugin('unit-test', $this->logger);
+ $this->plugin->initialize($this->server);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesExceptions')]
+ public function testLogging(string $expectedLogLevel, \Throwable $e): void {
+ $this->init();
+
+ $this->logger->expects($this->once())
+ ->method($expectedLogLevel)
+ ->with($e->getMessage(), ['app' => 'unit-test','exception' => $e]);
+
+ $this->plugin->logException($e);
+ }
+
+ public static function providesExceptions(): array {
+ return [
+ ['debug', new NotFound()],
+ ['debug', new ServerMaintenanceMode('System is in maintenance mode.')],
+ // Faking a translation
+ ['debug', new ServerMaintenanceMode('Syst3m 1s 1n m41nt3n4nc3 m0d3.')],
+ ['debug', new ServerMaintenanceMode('Upgrade needed')],
+ ['critical', new InvalidPath('This path leads to nowhere')]
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php
new file mode 100644
index 00000000000..366932137f4
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php
@@ -0,0 +1,160 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\FakeLockerPlugin;
+use Sabre\DAV\INode;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\Response;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+/**
+ * Class FakeLockerPluginTest
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre
+ */
+class FakeLockerPluginTest extends TestCase {
+ private FakeLockerPlugin $fakeLockerPlugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->fakeLockerPlugin = new FakeLockerPlugin();
+ }
+
+ public function testInitialize(): void {
+ /** @var Server $server */
+ $server = $this->createMock(Server::class);
+ $calls = [
+ ['method:LOCK', [$this->fakeLockerPlugin, 'fakeLockProvider'], 1],
+ ['method:UNLOCK', [$this->fakeLockerPlugin, 'fakeUnlockProvider'], 1],
+ ['propFind', [$this->fakeLockerPlugin, 'propFind'], 100],
+ ['validateTokens', [$this->fakeLockerPlugin, 'validateTokens'], 100],
+ ];
+ $server->expects($this->exactly(count($calls)))
+ ->method('on')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ $this->fakeLockerPlugin->initialize($server);
+ }
+
+ public function testGetHTTPMethods(): void {
+ $expected = [
+ 'LOCK',
+ 'UNLOCK',
+ ];
+ $this->assertSame($expected, $this->fakeLockerPlugin->getHTTPMethods('Test'));
+ }
+
+ public function testGetFeatures(): void {
+ $expected = [
+ 2,
+ ];
+ $this->assertSame($expected, $this->fakeLockerPlugin->getFeatures());
+ }
+
+ public function testPropFind(): void {
+ $propFind = $this->createMock(PropFind::class);
+ $node = $this->createMock(INode::class);
+
+ $calls = [
+ '{DAV:}supportedlock',
+ '{DAV:}lockdiscovery',
+ ];
+ $propFind->expects($this->exactly(count($calls)))
+ ->method('handle')
+ ->willReturnCallback(function ($propertyName) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, $propertyName);
+ });
+
+ $this->fakeLockerPlugin->propFind($propFind, $node);
+ }
+
+ public static function tokenDataProvider(): array {
+ return [
+ [
+ [
+ [
+ 'tokens' => [
+ [
+ 'token' => 'aToken',
+ 'validToken' => false,
+ ],
+ [],
+ [
+ 'token' => 'opaquelocktoken:asdf',
+ 'validToken' => false,
+ ]
+ ],
+ ]
+ ],
+ [
+ [
+ 'tokens' => [
+ [
+ 'token' => 'aToken',
+ 'validToken' => false,
+ ],
+ [],
+ [
+ 'token' => 'opaquelocktoken:asdf',
+ 'validToken' => true,
+ ]
+ ],
+ ]
+ ],
+ ]
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('tokenDataProvider')]
+ public function testValidateTokens(array $input, array $expected): void {
+ $request = $this->createMock(RequestInterface::class);
+ $this->fakeLockerPlugin->validateTokens($request, $input);
+ $this->assertSame($expected, $input);
+ }
+
+ public function testFakeLockProvider(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = new Response();
+ $server = $this->getMockBuilder(Server::class)
+ ->getMock();
+ $this->fakeLockerPlugin->initialize($server);
+
+ $request->expects($this->exactly(2))
+ ->method('getPath')
+ ->willReturn('MyPath');
+
+ $this->assertSame(false, $this->fakeLockerPlugin->fakeLockProvider($request, $response));
+
+ $expectedXml = '<?xml version="1.0" encoding="utf-8"?><d:prop xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"><d:lockdiscovery><d:activelock><d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype><d:lockroot><d:href>MyPath</d:href></d:lockroot><d:depth>infinity</d:depth><d:timeout>Second-1800</d:timeout><d:locktoken><d:href>opaquelocktoken:fe4f7f2437b151fbcb4e9f5c8118c6b1</d:href></d:locktoken></d:activelock></d:lockdiscovery></d:prop>';
+
+ $this->assertXmlStringEqualsXmlString($expectedXml, $response->getBody());
+ }
+
+ public function testFakeUnlockProvider(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ $response->expects($this->once())
+ ->method('setStatus')
+ ->with('204');
+ $response->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Length', '0');
+
+ $this->assertSame(false, $this->fakeLockerPlugin->fakeUnlockProvider($request, $response));
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/FileTest.php b/apps/dav/tests/unit/Connector/Sabre/FileTest.php
new file mode 100644
index 00000000000..60c8382e131
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/FileTest.php
@@ -0,0 +1,1031 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\AppFramework\Http\Request;
+use OC\Files\Filesystem;
+use OC\Files\Storage\Local;
+use OC\Files\Storage\Temporary;
+use OC\Files\Storage\Wrapper\PermissionsMask;
+use OC\Files\View;
+use OCA\DAV\Connector\Sabre\Exception\FileLocked;
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\DAV\Connector\Sabre\File;
+use OCP\Constants;
+use OCP\Encryption\Exceptions\GenericEncryptionException;
+use OCP\Files\EntityTooLargeException;
+use OCP\Files\FileInfo;
+use OCP\Files\ForbiddenException;
+use OCP\Files\InvalidContentException;
+use OCP\Files\InvalidPathException;
+use OCP\Files\LockNotAcquiredException;
+use OCP\Files\NotPermittedException;
+use OCP\Files\Storage\IStorage;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IConfig;
+use OCP\IRequestId;
+use OCP\ITempManager;
+use OCP\IUserManager;
+use OCP\Lock\ILockingProvider;
+use OCP\Lock\LockedException;
+use OCP\Server;
+use OCP\Util;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\HookHelper;
+use Test\TestCase;
+use Test\Traits\MountProviderTrait;
+use Test\Traits\UserTrait;
+
+/**
+ * Class File
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre
+ */
+class FileTest extends TestCase {
+ use MountProviderTrait;
+ use UserTrait;
+
+ private string $user;
+ protected IConfig&MockObject $config;
+ protected IRequestId&MockObject $requestId;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ \OC_Hook::clear();
+
+ $this->user = 'test_user';
+ $this->createUser($this->user, 'pass');
+
+ self::loginAsUser($this->user);
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->requestId = $this->createMock(IRequestId::class);
+ }
+
+ protected function tearDown(): void {
+ $userManager = Server::get(IUserManager::class);
+ $userManager->get($this->user)->delete();
+
+ parent::tearDown();
+ }
+
+ private function getMockStorage(): MockObject&IStorage {
+ $storage = $this->createMock(IStorage::class);
+ $storage->method('getId')
+ ->willReturn('home::someuser');
+ return $storage;
+ }
+
+ private function getStream(string $string) {
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, $string);
+ fseek($stream, 0);
+ return $stream;
+ }
+
+
+ public static function fopenFailuresProvider(): array {
+ return [
+ [
+ // return false
+ null,
+ '\Sabre\Dav\Exception',
+ false
+ ],
+ [
+ new NotPermittedException(),
+ 'Sabre\DAV\Exception\Forbidden'
+ ],
+ [
+ new EntityTooLargeException(),
+ 'OCA\DAV\Connector\Sabre\Exception\EntityTooLarge'
+ ],
+ [
+ new InvalidContentException(),
+ 'OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType'
+ ],
+ [
+ new InvalidPathException(),
+ 'Sabre\DAV\Exception\Forbidden'
+ ],
+ [
+ new ForbiddenException('', true),
+ 'OCA\DAV\Connector\Sabre\Exception\Forbidden'
+ ],
+ [
+ new LockNotAcquiredException('/test.txt', 1),
+ 'OCA\DAV\Connector\Sabre\Exception\FileLocked'
+ ],
+ [
+ new LockedException('/test.txt'),
+ 'OCA\DAV\Connector\Sabre\Exception\FileLocked'
+ ],
+ [
+ new GenericEncryptionException(),
+ 'Sabre\DAV\Exception\ServiceUnavailable'
+ ],
+ [
+ new StorageNotAvailableException(),
+ 'Sabre\DAV\Exception\ServiceUnavailable'
+ ],
+ [
+ new \Sabre\DAV\Exception('Generic sabre exception'),
+ 'Sabre\DAV\Exception',
+ false
+ ],
+ [
+ new \Exception('Generic exception'),
+ 'Sabre\DAV\Exception'
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('fopenFailuresProvider')]
+ public function testSimplePutFails(?\Throwable $thrownException, string $expectedException, bool $checkPreviousClass = true): void {
+ // setup
+ $storage = $this->getMockBuilder(Local::class)
+ ->onlyMethods(['writeStream'])
+ ->setConstructorArgs([['datadir' => Server::get(ITempManager::class)->getTemporaryFolder()]])
+ ->getMock();
+ Filesystem::mount($storage, [], $this->user . '/');
+ /** @var View&MockObject $view */
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['getRelativePath', 'resolvePath'])
+ ->getMock();
+ $view->expects($this->atLeastOnce())
+ ->method('resolvePath')
+ ->willReturnCallback(
+ function ($path) use ($storage) {
+ return [$storage, $path];
+ }
+ );
+
+ if ($thrownException !== null) {
+ $storage->expects($this->once())
+ ->method('writeStream')
+ ->willThrowException($thrownException);
+ } else {
+ $storage->expects($this->once())
+ ->method('writeStream')
+ ->willReturn(0);
+ }
+
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturnArgument(0);
+
+ $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+
+ $file = new File($view, $info);
+
+ // action
+ $caughtException = null;
+ try {
+ $file->put('test data');
+ } catch (\Exception $e) {
+ $caughtException = $e;
+ }
+
+ $this->assertInstanceOf($expectedException, $caughtException);
+ if ($checkPreviousClass) {
+ $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious());
+ }
+
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ * Simulate putting a file to the given path.
+ *
+ * @param string $path path to put the file into
+ * @param ?string $viewRoot root to use for the view
+ * @param null|Request $request the HTTP request
+ *
+ * @return null|string of the PUT operation which is usually the etag
+ */
+ private function doPut(string $path, ?string $viewRoot = null, ?Request $request = null) {
+ $view = Filesystem::getView();
+ if (!is_null($viewRoot)) {
+ $view = new View($viewRoot);
+ } else {
+ $viewRoot = '/' . $this->user . '/files';
+ }
+
+ $info = new \OC\Files\FileInfo(
+ $viewRoot . '/' . ltrim($path, '/'),
+ $this->getMockStorage(),
+ null,
+ [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ],
+ null
+ );
+
+ /** @var File&MockObject $file */
+ $file = $this->getMockBuilder(File::class)
+ ->setConstructorArgs([$view, $info, null, $request])
+ ->onlyMethods(['header'])
+ ->getMock();
+
+ // beforeMethod locks
+ $view->lockFile($path, ILockingProvider::LOCK_SHARED);
+
+ $result = $file->put($this->getStream('test data'));
+
+ // afterMethod unlocks
+ $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
+
+ return $result;
+ }
+
+ /**
+ * Test putting a single file
+ */
+ public function testPutSingleFile(): void {
+ $this->assertNotEmpty($this->doPut('/foo.txt'));
+ }
+
+ public static function legalMtimeProvider(): array {
+ return [
+ 'string' => [
+ 'requestMtime' => 'string',
+ 'resultMtime' => null
+ ],
+ 'castable string (int)' => [
+ 'requestMtime' => '987654321',
+ 'resultMtime' => 987654321
+ ],
+ 'castable string (float)' => [
+ 'requestMtime' => '123456789.56',
+ 'resultMtime' => 123456789
+ ],
+ 'float' => [
+ 'requestMtime' => 123456789.56,
+ 'resultMtime' => 123456789
+ ],
+ 'zero' => [
+ 'requestMtime' => 0,
+ 'resultMtime' => null
+ ],
+ 'zero string' => [
+ 'requestMtime' => '0',
+ 'resultMtime' => null
+ ],
+ 'negative zero string' => [
+ 'requestMtime' => '-0',
+ 'resultMtime' => null
+ ],
+ 'string starting with number following by char' => [
+ 'requestMtime' => '2345asdf',
+ 'resultMtime' => null
+ ],
+ 'string castable hex int' => [
+ 'requestMtime' => '0x45adf',
+ 'resultMtime' => null
+ ],
+ 'string that looks like invalid hex int' => [
+ 'requestMtime' => '0x123g',
+ 'resultMtime' => null
+ ],
+ 'negative int' => [
+ 'requestMtime' => -34,
+ 'resultMtime' => null
+ ],
+ 'negative float' => [
+ 'requestMtime' => -34.43,
+ 'resultMtime' => null
+ ],
+ ];
+ }
+
+ /**
+ * Test putting a file with string Mtime
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('legalMtimeProvider')]
+ public function testPutSingleFileLegalMtime(mixed $requestMtime, ?int $resultMtime): void {
+ $request = new Request([
+ 'server' => [
+ 'HTTP_X_OC_MTIME' => (string)$requestMtime,
+ ]
+ ], $this->requestId, $this->config, null);
+ $file = 'foo.txt';
+
+ if ($resultMtime === null) {
+ $this->expectException(\InvalidArgumentException::class);
+ }
+
+ $this->doPut($file, null, $request);
+
+ if ($resultMtime !== null) {
+ $this->assertEquals($resultMtime, $this->getFileInfos($file)['mtime']);
+ }
+ }
+
+ /**
+ * Test that putting a file triggers create hooks
+ */
+ public function testPutSingleFileTriggersHooks(): void {
+ HookHelper::setUpHooks();
+
+ $this->assertNotEmpty($this->doPut('/foo.txt'));
+
+ $this->assertCount(4, HookHelper::$hookCalls);
+ $this->assertHookCall(
+ HookHelper::$hookCalls[0],
+ Filesystem::signal_create,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[1],
+ Filesystem::signal_write,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[2],
+ Filesystem::signal_post_create,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[3],
+ Filesystem::signal_post_write,
+ '/foo.txt'
+ );
+ }
+
+ /**
+ * Test that putting a file triggers update hooks
+ */
+ public function testPutOverwriteFileTriggersHooks(): void {
+ $view = Filesystem::getView();
+ $view->file_put_contents('/foo.txt', 'some content that will be replaced');
+
+ HookHelper::setUpHooks();
+
+ $this->assertNotEmpty($this->doPut('/foo.txt'));
+
+ $this->assertCount(4, HookHelper::$hookCalls);
+ $this->assertHookCall(
+ HookHelper::$hookCalls[0],
+ Filesystem::signal_update,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[1],
+ Filesystem::signal_write,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[2],
+ Filesystem::signal_post_update,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[3],
+ Filesystem::signal_post_write,
+ '/foo.txt'
+ );
+ }
+
+ /**
+ * Test that putting a file triggers hooks with the correct path
+ * if the passed view was chrooted (can happen with public webdav
+ * where the root is the share root)
+ */
+ public function testPutSingleFileTriggersHooksDifferentRoot(): void {
+ $view = Filesystem::getView();
+ $view->mkdir('noderoot');
+
+ HookHelper::setUpHooks();
+
+ // happens with public webdav where the view root is the share root
+ $this->assertNotEmpty($this->doPut('/foo.txt', '/' . $this->user . '/files/noderoot'));
+
+ $this->assertCount(4, HookHelper::$hookCalls);
+ $this->assertHookCall(
+ HookHelper::$hookCalls[0],
+ Filesystem::signal_create,
+ '/noderoot/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[1],
+ Filesystem::signal_write,
+ '/noderoot/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[2],
+ Filesystem::signal_post_create,
+ '/noderoot/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[3],
+ Filesystem::signal_post_write,
+ '/noderoot/foo.txt'
+ );
+ }
+
+ public static function cancellingHook($params): void {
+ self::$hookCalls[] = [
+ 'signal' => Filesystem::signal_post_create,
+ 'params' => $params
+ ];
+ }
+
+ /**
+ * Test put file with cancelled hook
+ */
+ public function testPutSingleFileCancelPreHook(): void {
+ Util::connectHook(
+ Filesystem::CLASSNAME,
+ Filesystem::signal_create,
+ '\Test\HookHelper',
+ 'cancellingCallback'
+ );
+
+ // action
+ $thrown = false;
+ try {
+ $this->doPut('/foo.txt');
+ } catch (\Sabre\DAV\Exception $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles(), 'No stray part files');
+ }
+
+ /**
+ * Test exception when the uploaded size did not match
+ */
+ public function testSimplePutFailsSizeCheck(): void {
+ // setup
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['rename', 'getRelativePath', 'filesize'])
+ ->getMock();
+ $view->expects($this->any())
+ ->method('rename')
+ ->withAnyParameters()
+ ->willReturn(false);
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturnArgument(0);
+
+ $view->expects($this->any())
+ ->method('filesize')
+ ->willReturn(123456);
+
+ $request = new Request([
+ 'server' => [
+ 'CONTENT_LENGTH' => '123456',
+ ],
+ 'method' => 'PUT',
+ ], $this->requestId, $this->config, null);
+
+ $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+
+ $file = new File($view, $info, null, $request);
+
+ // action
+ $thrown = false;
+ try {
+ // beforeMethod locks
+ $file->acquireLock(ILockingProvider::LOCK_SHARED);
+
+ $file->put($this->getStream('test data'));
+
+ // afterMethod unlocks
+ $file->releaseLock(ILockingProvider::LOCK_SHARED);
+ } catch (\Sabre\DAV\Exception\BadRequest $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ * Test exception during final rename in simple upload mode
+ */
+ public function testSimplePutFailsMoveFromStorage(): void {
+ $view = new View('/' . $this->user . '/files');
+
+ // simulate situation where the target file is locked
+ $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE);
+
+ $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+
+ $file = new File($view, $info);
+
+ // action
+ $thrown = false;
+ try {
+ // beforeMethod locks
+ $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+
+ $file->put($this->getStream('test data'));
+
+ // afterMethod unlocks
+ $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+ } catch (FileLocked $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ * Test put file with invalid chars
+ */
+ public function testSimplePutInvalidChars(): void {
+ // setup
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['getRelativePath'])
+ ->getMock();
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturnArgument(0);
+
+ $info = new \OC\Files\FileInfo("/i\nvalid", $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+ $file = new File($view, $info);
+
+ // action
+ $thrown = false;
+ try {
+ // beforeMethod locks
+ $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+
+ $file->put($this->getStream('test data'));
+
+ // afterMethod unlocks
+ $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+ } catch (InvalidPath $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ * Test setting name with setName() with invalid chars
+ *
+ */
+ public function testSetNameInvalidChars(): void {
+ $this->expectException(InvalidPath::class);
+
+ // setup
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['getRelativePath'])
+ ->getMock();
+
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturnArgument(0);
+
+ $info = new \OC\Files\FileInfo('/valid', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+ $file = new File($view, $info);
+
+ $file->setName("/i\nvalid");
+ }
+
+
+ public function testUploadAbort(): void {
+ // setup
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['rename', 'getRelativePath', 'filesize'])
+ ->getMock();
+ $view->expects($this->any())
+ ->method('rename')
+ ->withAnyParameters()
+ ->willReturn(false);
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturnArgument(0);
+ $view->expects($this->any())
+ ->method('filesize')
+ ->willReturn(123456);
+
+ $request = new Request([
+ 'server' => [
+ 'CONTENT_LENGTH' => '123456',
+ ],
+ 'method' => 'PUT',
+ ], $this->requestId, $this->config, null);
+
+ $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+
+ $file = new File($view, $info, null, $request);
+
+ // action
+ $thrown = false;
+ try {
+ // beforeMethod locks
+ $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+
+ $file->put($this->getStream('test data'));
+
+ // afterMethod unlocks
+ $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+ } catch (\Sabre\DAV\Exception\BadRequest $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+
+ public function testDeleteWhenAllowed(): void {
+ // setup
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->getMock();
+
+ $view->expects($this->once())
+ ->method('unlink')
+ ->willReturn(true);
+
+ $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+
+ $file = new File($view, $info);
+
+ // action
+ $file->delete();
+ }
+
+
+ public function testDeleteThrowsWhenDeletionNotAllowed(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ // setup
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->getMock();
+
+ $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => 0,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+
+ $file = new File($view, $info);
+
+ // action
+ $file->delete();
+ }
+
+
+ public function testDeleteThrowsWhenDeletionFailed(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ // setup
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->getMock();
+
+ // but fails
+ $view->expects($this->once())
+ ->method('unlink')
+ ->willReturn(false);
+
+ $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+
+ $file = new File($view, $info);
+
+ // action
+ $file->delete();
+ }
+
+
+ public function testDeleteThrowsWhenDeletionThrows(): void {
+ $this->expectException(Forbidden::class);
+
+ // setup
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->getMock();
+
+ // but fails
+ $view->expects($this->once())
+ ->method('unlink')
+ ->willThrowException(new ForbiddenException('', true));
+
+ $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+
+ $file = new File($view, $info);
+
+ // action
+ $file->delete();
+ }
+
+ /**
+ * Asserts hook call
+ *
+ * @param array $callData hook call data to check
+ * @param string $signal signal name
+ * @param string $hookPath hook path
+ */
+ protected function assertHookCall($callData, $signal, $hookPath) {
+ $this->assertEquals($signal, $callData['signal']);
+ $params = $callData['params'];
+ $this->assertEquals(
+ $hookPath,
+ $params[Filesystem::signal_param_path]
+ );
+ }
+
+ /**
+ * Test whether locks are set before and after the operation
+ */
+ public function testPutLocking(): void {
+ $view = new View('/' . $this->user . '/files/');
+
+ $path = 'test-locking.txt';
+ $info = new \OC\Files\FileInfo(
+ '/' . $this->user . '/files/' . $path,
+ $this->getMockStorage(),
+ null,
+ [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ],
+ null
+ );
+
+ $file = new File($view, $info);
+
+ $this->assertFalse(
+ $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED),
+ 'File unlocked before put'
+ );
+ $this->assertFalse(
+ $this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE),
+ 'File unlocked before put'
+ );
+
+ $wasLockedPre = false;
+ $wasLockedPost = false;
+ $eventHandler = $this->getMockBuilder(\stdclass::class)
+ ->addMethods(['writeCallback', 'postWriteCallback'])
+ ->getMock();
+
+ // both pre and post hooks might need access to the file,
+ // so only shared lock is acceptable
+ $eventHandler->expects($this->once())
+ ->method('writeCallback')
+ ->willReturnCallback(
+ function () use ($view, $path, &$wasLockedPre): void {
+ $wasLockedPre = $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED);
+ $wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE);
+ }
+ );
+ $eventHandler->expects($this->once())
+ ->method('postWriteCallback')
+ ->willReturnCallback(
+ function () use ($view, $path, &$wasLockedPost): void {
+ $wasLockedPost = $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED);
+ $wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE);
+ }
+ );
+
+ Util::connectHook(
+ Filesystem::CLASSNAME,
+ Filesystem::signal_write,
+ $eventHandler,
+ 'writeCallback'
+ );
+ Util::connectHook(
+ Filesystem::CLASSNAME,
+ Filesystem::signal_post_write,
+ $eventHandler,
+ 'postWriteCallback'
+ );
+
+ // beforeMethod locks
+ $view->lockFile($path, ILockingProvider::LOCK_SHARED);
+
+ $this->assertNotEmpty($file->put($this->getStream('test data')));
+
+ // afterMethod unlocks
+ $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
+
+ $this->assertTrue($wasLockedPre, 'File was locked during pre-hooks');
+ $this->assertTrue($wasLockedPost, 'File was locked during post-hooks');
+
+ $this->assertFalse(
+ $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED),
+ 'File unlocked after put'
+ );
+ $this->assertFalse(
+ $this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE),
+ 'File unlocked after put'
+ );
+ }
+
+ /**
+ * Returns part files in the given path
+ *
+ * @param \OC\Files\View view which root is the current user's "files" folder
+ * @param string $path path for which to list part files
+ *
+ * @return array list of part files
+ */
+ private function listPartFiles(?View $userView = null, $path = '') {
+ if ($userView === null) {
+ $userView = Filesystem::getView();
+ }
+ $files = [];
+ [$storage, $internalPath] = $userView->resolvePath($path);
+ if ($storage instanceof Local) {
+ $realPath = $storage->getSourcePath($internalPath);
+ $dh = opendir($realPath);
+ while (($file = readdir($dh)) !== false) {
+ if (str_ends_with($file, '.part')) {
+ $files[] = $file;
+ }
+ }
+ closedir($dh);
+ }
+ return $files;
+ }
+
+ /**
+ * returns an array of file information filesize, mtime, filetype, mimetype
+ *
+ * @param string $path
+ * @param View $userView
+ * @return array
+ */
+ private function getFileInfos($path = '', ?View $userView = null) {
+ if ($userView === null) {
+ $userView = Filesystem::getView();
+ }
+ return [
+ 'filesize' => $userView->filesize($path),
+ 'mtime' => $userView->filemtime($path),
+ 'filetype' => $userView->filetype($path),
+ 'mimetype' => $userView->getMimeType($path)
+ ];
+ }
+
+
+ public function testGetFopenFails(): void {
+ $this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class);
+
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['fopen'])
+ ->getMock();
+ $view->expects($this->atLeastOnce())
+ ->method('fopen')
+ ->willReturn(false);
+
+ $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FILE,
+ ], null);
+
+ $file = new File($view, $info);
+
+ $file->get();
+ }
+
+
+ public function testGetFopenThrows(): void {
+ $this->expectException(Forbidden::class);
+
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['fopen'])
+ ->getMock();
+ $view->expects($this->atLeastOnce())
+ ->method('fopen')
+ ->willThrowException(new ForbiddenException('', true));
+
+ $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FILE,
+ ], null);
+
+ $file = new File($view, $info);
+
+ $file->get();
+ }
+
+
+ public function testGetThrowsIfNoPermission(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ /** @var View&MockObject */
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['fopen'])
+ ->getMock();
+ $view->expects($this->never())
+ ->method('fopen');
+
+ $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_CREATE, // no read perm
+ 'type' => FileInfo::TYPE_FOLDER,
+ ], null);
+
+ $file = new File($view, $info);
+
+ $file->get();
+ }
+
+ public function testSimplePutNoCreatePermissions(): void {
+ $this->logout();
+
+ $storage = new Temporary([]);
+ $storage->file_put_contents('file.txt', 'old content');
+ $noCreateStorage = new PermissionsMask([
+ 'storage' => $storage,
+ 'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE
+ ]);
+
+ $this->registerMount($this->user, $noCreateStorage, '/' . $this->user . '/files/root');
+
+ $this->loginAsUser($this->user);
+
+ $view = new View('/' . $this->user . '/files');
+
+ $info = $view->getFileInfo('root/file.txt');
+
+ $file = new File($view, $info);
+
+ // beforeMethod locks
+ $view->lockFile('root/file.txt', ILockingProvider::LOCK_SHARED);
+
+ $file->put($this->getStream('new content'));
+
+ // afterMethod unlocks
+ $view->unlockFile('root/file.txt', ILockingProvider::LOCK_SHARED);
+
+ $this->assertEquals('new content', $view->file_get_contents('root/file.txt'));
+ }
+
+ public function testPutLockExpired(): void {
+ $view = new View('/' . $this->user . '/files/');
+
+ $path = 'test-locking.txt';
+ $info = new \OC\Files\FileInfo(
+ '/' . $this->user . '/files/' . $path,
+ $this->getMockStorage(),
+ null,
+ [
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FOLDER,
+ ],
+ null
+ );
+
+ $file = new File($view, $info);
+
+ // don't lock before the PUT to simulate an expired shared lock
+ $this->assertNotEmpty($file->put($this->getStream('test data')));
+
+ // afterMethod unlocks
+ $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
new file mode 100644
index 00000000000..4df3accfda9
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
@@ -0,0 +1,720 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\Accounts\Account;
+use OC\Accounts\AccountProperty;
+use OC\User\User;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\DAV\Connector\Sabre\File;
+use OCA\DAV\Connector\Sabre\FilesPlugin;
+use OCA\DAV\Connector\Sabre\Node;
+use OCP\Accounts\IAccountManager;
+use OCP\Files\FileInfo;
+use OCP\Files\IFilenameValidator;
+use OCP\Files\InvalidPathException;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IConfig;
+use OCP\IPreview;
+use OCP\IRequest;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\PropPatch;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\Xml\Service;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class FilesPluginTest extends TestCase {
+
+ private Tree&MockObject $tree;
+ private Server&MockObject $server;
+ private IConfig&MockObject $config;
+ private IRequest&MockObject $request;
+ private IPreview&MockObject $previewManager;
+ private IUserSession&MockObject $userSession;
+ private IFilenameValidator&MockObject $filenameValidator;
+ private IAccountManager&MockObject $accountManager;
+ private FilesPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->server = $this->createMock(Server::class);
+ $this->tree = $this->createMock(Tree::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->config->expects($this->any())->method('getSystemValue')
+ ->with($this->equalTo('data-fingerprint'), $this->equalTo(''))
+ ->willReturn('my_fingerprint');
+ $this->request = $this->createMock(IRequest::class);
+ $this->previewManager = $this->createMock(IPreview::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->filenameValidator = $this->createMock(IFilenameValidator::class);
+ $this->accountManager = $this->createMock(IAccountManager::class);
+
+ $this->plugin = new FilesPlugin(
+ $this->tree,
+ $this->config,
+ $this->request,
+ $this->previewManager,
+ $this->userSession,
+ $this->filenameValidator,
+ $this->accountManager,
+ );
+
+ $response = $this->createMock(ResponseInterface::class);
+ $this->server->httpResponse = $response;
+ $this->server->xml = new Service();
+
+ $this->plugin->initialize($this->server);
+ }
+
+ private function createTestNode(string $class, string $path = '/dummypath'): MockObject {
+ $node = $this->createMock($class);
+
+ $node->expects($this->any())
+ ->method('getId')
+ ->willReturn(123);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with($path)
+ ->willReturn($node);
+
+ $node->expects($this->any())
+ ->method('getFileId')
+ ->willReturn('00000123instanceid');
+ $node->expects($this->any())
+ ->method('getInternalFileId')
+ ->willReturn('123');
+ $node->expects($this->any())
+ ->method('getEtag')
+ ->willReturn('"abc"');
+ $node->expects($this->any())
+ ->method('getDavPermissions')
+ ->willReturn('DWCKMSR');
+
+ $fileInfo = $this->createMock(FileInfo::class);
+ $fileInfo->expects($this->any())
+ ->method('isReadable')
+ ->willReturn(true);
+ $fileInfo->expects($this->any())
+ ->method('getCreationTime')
+ ->willReturn(123456789);
+
+ $node->expects($this->any())
+ ->method('getFileInfo')
+ ->willReturn($fileInfo);
+
+ return $node;
+ }
+
+ public function testGetPropertiesForFile(): void {
+ /** @var File&MockObject $node */
+ $node = $this->createTestNode(File::class);
+
+ $propFind = new PropFind(
+ '/dummyPath',
+ [
+ FilesPlugin::GETETAG_PROPERTYNAME,
+ FilesPlugin::FILEID_PROPERTYNAME,
+ FilesPlugin::INTERNAL_FILEID_PROPERTYNAME,
+ FilesPlugin::SIZE_PROPERTYNAME,
+ FilesPlugin::PERMISSIONS_PROPERTYNAME,
+ FilesPlugin::DOWNLOADURL_PROPERTYNAME,
+ FilesPlugin::OWNER_ID_PROPERTYNAME,
+ FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME,
+ FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME,
+ FilesPlugin::CREATIONDATE_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ $user = $this->createMock(User::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('foo');
+ $user
+ ->expects($this->once())
+ ->method('getDisplayName')
+ ->willReturn('M. Foo');
+
+ $owner = $this->createMock(Account::class);
+ $this->accountManager->expects($this->once())
+ ->method('getAccount')
+ ->with($user)
+ ->willReturn($owner);
+
+ $node->expects($this->once())
+ ->method('getDirectDownload')
+ ->willReturn(['url' => 'http://example.com/']);
+ $node->expects($this->exactly(2))
+ ->method('getOwner')
+ ->willReturn($user);
+
+ $displayNameProp = $this->createMock(AccountProperty::class);
+ $owner
+ ->expects($this->once())
+ ->method('getProperty')
+ ->with(IAccountManager::PROPERTY_DISPLAYNAME)
+ ->willReturn($displayNameProp);
+ $displayNameProp
+ ->expects($this->once())
+ ->method('getScope')
+ ->willReturn(IAccountManager::SCOPE_PUBLISHED);
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals('"abc"', $propFind->get(FilesPlugin::GETETAG_PROPERTYNAME));
+ $this->assertEquals('00000123instanceid', $propFind->get(FilesPlugin::FILEID_PROPERTYNAME));
+ $this->assertEquals('123', $propFind->get(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME));
+ $this->assertEquals('1973-11-29T21:33:09+00:00', $propFind->get(FilesPlugin::CREATIONDATE_PROPERTYNAME));
+ $this->assertEquals(0, $propFind->get(FilesPlugin::SIZE_PROPERTYNAME));
+ $this->assertEquals('DWCKMSR', $propFind->get(FilesPlugin::PERMISSIONS_PROPERTYNAME));
+ $this->assertEquals('http://example.com/', $propFind->get(FilesPlugin::DOWNLOADURL_PROPERTYNAME));
+ $this->assertEquals('foo', $propFind->get(FilesPlugin::OWNER_ID_PROPERTYNAME));
+ $this->assertEquals('M. Foo', $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME));
+ $this->assertEquals('my_fingerprint', $propFind->get(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME));
+ $this->assertEquals([], $propFind->get404Properties());
+ }
+
+ public function testGetDisplayNamePropertyWhenNotPublished(): void {
+ $node = $this->createTestNode(File::class);
+ $propFind = new PropFind(
+ '/dummyPath',
+ [
+ FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn(null);
+
+ $user = $this->createMock(User::class);
+
+ $user->expects($this->never())
+ ->method('getDisplayName');
+
+ $owner = $this->createMock(Account::class);
+ $this->accountManager->expects($this->once())
+ ->method('getAccount')
+ ->with($user)
+ ->willReturn($owner);
+
+ $node->expects($this->once())
+ ->method('getOwner')
+ ->willReturn($user);
+
+ $displayNameProp = $this->createMock(AccountProperty::class);
+ $owner
+ ->expects($this->once())
+ ->method('getProperty')
+ ->with(IAccountManager::PROPERTY_DISPLAYNAME)
+ ->willReturn($displayNameProp);
+ $displayNameProp
+ ->expects($this->once())
+ ->method('getScope')
+ ->willReturn(IAccountManager::SCOPE_PRIVATE);
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals(null, $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME));
+ }
+
+ public function testGetDisplayNamePropertyWhenNotPublishedButLoggedIn(): void {
+ $node = $this->createTestNode(File::class);
+
+ $propFind = new PropFind(
+ '/dummyPath',
+ [
+ FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ $user = $this->createMock(User::class);
+
+ $node->expects($this->once())
+ ->method('getOwner')
+ ->willReturn($user);
+
+ $loggedInUser = $this->createMock(User::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($loggedInUser);
+
+ $user
+ ->expects($this->once())
+ ->method('getDisplayName')
+ ->willReturn('M. Foo');
+
+ $this->accountManager->expects($this->never())
+ ->method('getAccount');
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals('M. Foo', $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME));
+ }
+
+ public function testGetPropertiesStorageNotAvailable(): void {
+ /** @var File&MockObject $node */
+ $node = $this->createTestNode(File::class);
+
+ $propFind = new PropFind(
+ '/dummyPath',
+ [
+ FilesPlugin::DOWNLOADURL_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ $node->expects($this->once())
+ ->method('getDirectDownload')
+ ->willThrowException(new StorageNotAvailableException());
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals(null, $propFind->get(FilesPlugin::DOWNLOADURL_PROPERTYNAME));
+ }
+
+ public function testGetPublicPermissions(): void {
+ /** @var IRequest&MockObject */
+ $request = $this->createMock(IRequest::class);
+ $this->plugin = new FilesPlugin(
+ $this->tree,
+ $this->config,
+ $request,
+ $this->previewManager,
+ $this->userSession,
+ $this->filenameValidator,
+ $this->accountManager,
+ true,
+ );
+ $this->plugin->initialize($this->server);
+
+ $propFind = new PropFind(
+ '/dummyPath',
+ [
+ FilesPlugin::PERMISSIONS_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ /** @var File&MockObject $node */
+ $node = $this->createTestNode(File::class);
+ $node->expects($this->any())
+ ->method('getDavPermissions')
+ ->willReturn('DWCKMSR');
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals('DWCKR', $propFind->get(FilesPlugin::PERMISSIONS_PROPERTYNAME));
+ }
+
+ public function testGetPropertiesForDirectory(): void {
+ /** @var Directory&MockObject $node */
+ $node = $this->createTestNode(Directory::class);
+
+ $propFind = new PropFind(
+ '/dummyPath',
+ [
+ FilesPlugin::GETETAG_PROPERTYNAME,
+ FilesPlugin::FILEID_PROPERTYNAME,
+ FilesPlugin::SIZE_PROPERTYNAME,
+ FilesPlugin::PERMISSIONS_PROPERTYNAME,
+ FilesPlugin::DOWNLOADURL_PROPERTYNAME,
+ FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ $node->expects($this->once())
+ ->method('getSize')
+ ->willReturn(1025);
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals('"abc"', $propFind->get(FilesPlugin::GETETAG_PROPERTYNAME));
+ $this->assertEquals('00000123instanceid', $propFind->get(FilesPlugin::FILEID_PROPERTYNAME));
+ $this->assertEquals(1025, $propFind->get(FilesPlugin::SIZE_PROPERTYNAME));
+ $this->assertEquals('DWCKMSR', $propFind->get(FilesPlugin::PERMISSIONS_PROPERTYNAME));
+ $this->assertEquals(null, $propFind->get(FilesPlugin::DOWNLOADURL_PROPERTYNAME));
+ $this->assertEquals('my_fingerprint', $propFind->get(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME));
+ $this->assertEquals([FilesPlugin::DOWNLOADURL_PROPERTYNAME], $propFind->get404Properties());
+ }
+
+ public function testGetPropertiesForRootDirectory(): void {
+ /** @var Directory&MockObject $node */
+ $node = $this->createMock(Directory::class);
+ $node->expects($this->any())->method('getPath')->willReturn('/');
+
+ $fileInfo = $this->createMock(FileInfo::class);
+ $fileInfo->expects($this->any())
+ ->method('isReadable')
+ ->willReturn(true);
+
+ $node->expects($this->any())
+ ->method('getFileInfo')
+ ->willReturn($fileInfo);
+
+ $propFind = new PropFind(
+ '/',
+ [
+ FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals('my_fingerprint', $propFind->get(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME));
+ }
+
+ public function testGetPropertiesWhenNoPermission(): void {
+ // No read permissions can be caused by files access control.
+ // But we still want to load the directory list, so this is okay for us.
+ // $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ /** @var Directory&MockObject $node */
+ $node = $this->createMock(Directory::class);
+ $node->expects($this->any())->method('getPath')->willReturn('/');
+
+ $fileInfo = $this->createMock(FileInfo::class);
+ $fileInfo->expects($this->any())
+ ->method('isReadable')
+ ->willReturn(false);
+
+ $node->expects($this->any())
+ ->method('getFileInfo')
+ ->willReturn($fileInfo);
+
+ $propFind = new PropFind(
+ '/test',
+ [
+ FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->addToAssertionCount(1);
+ }
+
+ public function testUpdateProps(): void {
+ $node = $this->createTestNode(File::class);
+
+ $testDate = 'Fri, 13 Feb 2015 00:01:02 GMT';
+ $testCreationDate = '2007-08-31T16:47+00:00';
+
+ $node->expects($this->once())
+ ->method('touch')
+ ->with($testDate);
+
+ $node->expects($this->once())
+ ->method('setEtag')
+ ->with('newetag')
+ ->willReturn(true);
+
+ $node->expects($this->once())
+ ->method('setCreationTime')
+ ->with('1188578820');
+
+ // properties to set
+ $propPatch = new PropPatch([
+ FilesPlugin::GETETAG_PROPERTYNAME => 'newetag',
+ FilesPlugin::LASTMODIFIED_PROPERTYNAME => $testDate,
+ FilesPlugin::CREATIONDATE_PROPERTYNAME => $testCreationDate,
+ ]);
+
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(200, $result[FilesPlugin::LASTMODIFIED_PROPERTYNAME]);
+ $this->assertEquals(200, $result[FilesPlugin::GETETAG_PROPERTYNAME]);
+ $this->assertEquals(200, $result[FilesPlugin::CREATIONDATE_PROPERTYNAME]);
+ }
+
+ public function testUpdatePropsForbidden(): void {
+ $propPatch = new PropPatch([
+ FilesPlugin::OWNER_ID_PROPERTYNAME => 'user2',
+ FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME => 'User Two',
+ FilesPlugin::FILEID_PROPERTYNAME => 12345,
+ FilesPlugin::PERMISSIONS_PROPERTYNAME => 'C',
+ FilesPlugin::SIZE_PROPERTYNAME => 123,
+ FilesPlugin::DOWNLOADURL_PROPERTYNAME => 'http://example.com/',
+ ]);
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(403, $result[FilesPlugin::OWNER_ID_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::FILEID_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::PERMISSIONS_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::SIZE_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::DOWNLOADURL_PROPERTYNAME]);
+ }
+
+ /**
+ * Test case from https://github.com/owncloud/core/issues/5251
+ *
+ * |-FolderA
+ * |-text.txt
+ * |-test.txt
+ *
+ * FolderA is an incoming shared folder and there are no delete permissions.
+ * Thus moving /FolderA/test.txt to /test.txt should fail already on that check
+ *
+ */
+ public function testMoveSrcNotDeletable(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('FolderA/test.txt cannot be deleted');
+
+ $fileInfoFolderATestTXT = $this->createMock(FileInfo::class);
+ $fileInfoFolderATestTXT->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(false);
+
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
+ ->method('getFileInfo')
+ ->willReturn($fileInfoFolderATestTXT);
+
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
+ ->willReturn($node);
+
+ $this->plugin->checkMove('FolderA/test.txt', 'test.txt');
+ }
+
+ public function testMoveSrcDeletable(): void {
+ $fileInfoFolderATestTXT = $this->createMock(FileInfo::class);
+ $fileInfoFolderATestTXT->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(true);
+
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
+ ->method('getFileInfo')
+ ->willReturn($fileInfoFolderATestTXT);
+
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
+ ->willReturn($node);
+
+ $this->plugin->checkMove('FolderA/test.txt', 'test.txt');
+ }
+
+ public function testMoveSrcNotExist(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ $this->expectExceptionMessage('FolderA/test.txt does not exist');
+
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
+ ->method('getFileInfo')
+ ->willReturn(null);
+
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
+ ->willReturn($node);
+
+ $this->plugin->checkMove('FolderA/test.txt', 'test.txt');
+ }
+
+ public function testMoveDestinationInvalid(): void {
+ $this->expectException(InvalidPath::class);
+ $this->expectExceptionMessage('Mocked exception');
+
+ $fileInfoFolderATestTXT = $this->createMock(FileInfo::class);
+ $fileInfoFolderATestTXT->expects(self::any())
+ ->method('isDeletable')
+ ->willReturn(true);
+
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
+ ->method('getFileInfo')
+ ->willReturn($fileInfoFolderATestTXT);
+
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
+ ->willReturn($node);
+
+ $this->filenameValidator->expects(self::once())
+ ->method('validateFilename')
+ ->with('invalid\\path.txt')
+ ->willThrowException(new InvalidPathException('Mocked exception'));
+
+ $this->plugin->checkMove('FolderA/test.txt', 'invalid\\path.txt');
+ }
+
+ public function testCopySrcNotExist(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ $this->expectExceptionMessage('FolderA/test.txt does not exist');
+
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
+ ->method('getFileInfo')
+ ->willReturn(null);
+
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
+ ->willReturn($node);
+
+ $this->plugin->checkCopy('FolderA/test.txt', 'test.txt');
+ }
+
+ public function testCopyDestinationInvalid(): void {
+ $this->expectException(InvalidPath::class);
+ $this->expectExceptionMessage('Mocked exception');
+
+ $fileInfoFolderATestTXT = $this->createMock(FileInfo::class);
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
+ ->method('getFileInfo')
+ ->willReturn($fileInfoFolderATestTXT);
+
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
+ ->willReturn($node);
+
+ $this->filenameValidator->expects(self::once())
+ ->method('validateFilename')
+ ->with('invalid\\path.txt')
+ ->willThrowException(new InvalidPathException('Mocked exception'));
+
+ $this->plugin->checkCopy('FolderA/test.txt', 'invalid\\path.txt');
+ }
+
+ public static function downloadHeadersProvider(): array {
+ return [
+ [
+ false,
+ 'attachment; filename*=UTF-8\'\'somefile.xml; filename="somefile.xml"'
+ ],
+ [
+ true,
+ 'attachment; filename="somefile.xml"'
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('downloadHeadersProvider')]
+ public function testDownloadHeaders(bool $isClumsyAgent, string $contentDispositionHeader): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ $request
+ ->expects($this->once())
+ ->method('getPath')
+ ->willReturn('test/somefile.xml');
+
+ $node = $this->createMock(File::class);
+ $node
+ ->expects($this->once())
+ ->method('getName')
+ ->willReturn('somefile.xml');
+
+ $this->tree
+ ->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('test/somefile.xml')
+ ->willReturn($node);
+
+ $this->request
+ ->expects($this->once())
+ ->method('isUserAgent')
+ ->willReturn($isClumsyAgent);
+
+ $calls = [
+ ['Content-Disposition', $contentDispositionHeader],
+ ['X-Accel-Buffering', 'no'],
+ ];
+ $response
+ ->expects($this->exactly(count($calls)))
+ ->method('addHeader')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertSame($expected, func_get_args());
+ });
+
+ $this->plugin->httpGet($request, $response);
+ }
+
+ public function testHasPreview(): void {
+ /** @var Directory&MockObject $node */
+ $node = $this->createTestNode(Directory::class);
+
+ $propFind = new PropFind(
+ '/dummyPath',
+ [
+ FilesPlugin::HAS_PREVIEW_PROPERTYNAME
+ ],
+ 0
+ );
+
+ $this->previewManager->expects($this->once())
+ ->method('isAvailable')
+ ->willReturn(false);
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals('false', $propFind->get(FilesPlugin::HAS_PREVIEW_PROPERTYNAME));
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
new file mode 100644
index 00000000000..176949f999c
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
@@ -0,0 +1,853 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\Files\View;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\FilesPlugin;
+use OCA\DAV\Connector\Sabre\FilesReportPlugin as FilesReportPluginImplementation;
+use OCP\Accounts\IAccountManager;
+use OCP\App\IAppManager;
+use OCP\Files\File;
+use OCP\Files\FileInfo;
+use OCP\Files\Folder;
+use OCP\Files\IFilenameValidator;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IPreview;
+use OCP\IRequest;
+use OCP\ITagManager;
+use OCP\ITags;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
+use OCP\SystemTag\TagNotFoundException;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\INode;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\ResponseInterface;
+
+class FilesReportPluginTest extends \Test\TestCase {
+
+ private \Sabre\DAV\Server&MockObject $server;
+ private Tree&MockObject $tree;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private ISystemTagManager&MockObject $tagManager;
+ private ITags&MockObject $privateTags;
+ private ITagManager&MockObject $privateTagManager;
+ private IUserSession&MockObject $userSession;
+ private FilesReportPluginImplementation $plugin;
+ private View&MockObject $view;
+ private IGroupManager&MockObject $groupManager;
+ private Folder&MockObject $userFolder;
+ private IPreview&MockObject $previewManager;
+ private IAppManager&MockObject $appManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->tree = $this->createMock(Tree::class);
+ $this->view = $this->createMock(View::class);
+
+ $this->server = $this->getMockBuilder(Server::class)
+ ->setConstructorArgs([$this->tree])
+ ->onlyMethods(['getRequestUri', 'getBaseUri'])
+ ->getMock();
+
+ $this->server->expects($this->any())
+ ->method('getBaseUri')
+ ->willReturn('http://example.com/owncloud/remote.php/dav');
+
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->userFolder = $this->createMock(Folder::class);
+ $this->previewManager = $this->createMock(IPreview::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->privateTags = $this->createMock(ITags::class);
+ $this->privateTagManager = $this->createMock(ITagManager::class);
+ $this->privateTagManager->expects($this->any())
+ ->method('load')
+ ->with('files')
+ ->willReturn($this->privateTags);
+
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('testuser');
+ $this->userSession->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->plugin = new FilesReportPluginImplementation(
+ $this->tree,
+ $this->view,
+ $this->tagManager,
+ $this->tagMapper,
+ $this->privateTagManager,
+ $this->userSession,
+ $this->groupManager,
+ $this->userFolder,
+ $this->appManager
+ );
+ }
+
+ public function testOnReportInvalidNode(): void {
+ $path = 'totally/unrelated/13';
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/' . $path)
+ ->willReturn($this->createMock(INode::class));
+
+ $this->server->expects($this->any())
+ ->method('getRequestUri')
+ ->willReturn($path);
+ $this->plugin->initialize($this->server);
+
+ $this->assertNull($this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, [], '/' . $path));
+ }
+
+ public function testOnReportInvalidReportName(): void {
+ $path = 'test';
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/' . $path)
+ ->willReturn(
+ $this->getMockBuilder(INode::class)
+ ->disableOriginalConstructor()
+ ->getMock()
+ );
+
+ $this->server->expects($this->any())
+ ->method('getRequestUri')
+ ->willReturn($path);
+ $this->plugin->initialize($this->server);
+
+ $this->assertNull($this->plugin->onReport('{whoever}whatever', [], '/' . $path));
+ }
+
+ public function testOnReport(): void {
+ $path = 'test';
+
+ $parameters = [
+ [
+ 'name' => '{DAV:}prop',
+ 'value' => [
+ ['name' => '{DAV:}getcontentlength', 'value' => ''],
+ ['name' => '{http://owncloud.org/ns}size', 'value' => ''],
+ ],
+ ],
+ [
+ 'name' => '{http://owncloud.org/ns}filter-rules',
+ 'value' => [
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
+ ],
+ ],
+ ];
+
+ $this->groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->willReturn(true);
+
+ $reportTargetNode = $this->createMock(Directory::class);
+ $reportTargetNode->expects($this->any())
+ ->method('getPath')
+ ->willReturn('');
+
+ $response = $this->createMock(ResponseInterface::class);
+
+ $response->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Type', 'application/xml; charset=utf-8');
+
+ $response->expects($this->once())
+ ->method('setStatus')
+ ->with(207);
+
+ $response->expects($this->once())
+ ->method('setBody');
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/' . $path)
+ ->willReturn($reportTargetNode);
+
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag123, $tag456]);
+
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
+ ->willReturnMap([
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1]],
+ ['FourFiveSix', 'testuser', 0, 0, [$filesNode2]],
+ ]);
+
+ $this->server->expects($this->any())
+ ->method('getRequestUri')
+ ->willReturn($path);
+ $this->server->httpResponse = $response;
+ $this->plugin->initialize($this->server);
+
+ $this->assertFalse($this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, $parameters, '/' . $path));
+ }
+
+ public function testFindNodesByFileIdsRoot(): void {
+ $filesNode1 = $this->createMock(Folder::class);
+ $filesNode1->expects($this->once())
+ ->method('getName')
+ ->willReturn('first node');
+
+ $filesNode2 = $this->createMock(File::class);
+ $filesNode2->expects($this->once())
+ ->method('getName')
+ ->willReturn('second node');
+
+ $reportTargetNode = $this->createMock(Directory::class);
+ $reportTargetNode->expects($this->any())
+ ->method('getPath')
+ ->willReturn('/');
+
+ $this->userFolder->expects($this->exactly(2))
+ ->method('getFirstNodeById')
+ ->willReturnMap([
+ [111, $filesNode1],
+ [222, $filesNode2],
+ ]);
+
+ /** @var Directory&MockObject $reportTargetNode */
+ $result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']);
+
+ $this->assertCount(2, $result);
+ $this->assertInstanceOf(Directory::class, $result[0]);
+ $this->assertEquals('first node', $result[0]->getName());
+ $this->assertInstanceOf(\OCA\DAV\Connector\Sabre\File::class, $result[1]);
+ $this->assertEquals('second node', $result[1]->getName());
+ }
+
+ public function testFindNodesByFileIdsSubDir(): void {
+ $filesNode1 = $this->createMock(Folder::class);
+ $filesNode1->expects($this->once())
+ ->method('getName')
+ ->willReturn('first node');
+
+ $filesNode2 = $this->createMock(File::class);
+ $filesNode2->expects($this->once())
+ ->method('getName')
+ ->willReturn('second node');
+
+ $reportTargetNode = $this->createMock(Directory::class);
+ $reportTargetNode->expects($this->any())
+ ->method('getPath')
+ ->willReturn('/sub1/sub2');
+
+
+ $subNode = $this->createMock(Folder::class);
+
+ $this->userFolder->expects($this->once())
+ ->method('get')
+ ->with('/sub1/sub2')
+ ->willReturn($subNode);
+
+ $subNode->expects($this->exactly(2))
+ ->method('getFirstNodeById')
+ ->willReturnMap([
+ [111, $filesNode1],
+ [222, $filesNode2],
+ ]);
+
+ /** @var Directory&MockObject $reportTargetNode */
+ $result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']);
+
+ $this->assertCount(2, $result);
+ $this->assertInstanceOf(Directory::class, $result[0]);
+ $this->assertEquals('first node', $result[0]->getName());
+ $this->assertInstanceOf(\OCA\DAV\Connector\Sabre\File::class, $result[1]);
+ $this->assertEquals('second node', $result[1]->getName());
+ }
+
+ public function testPrepareResponses(): void {
+ $requestedProps = ['{DAV:}getcontentlength', '{http://owncloud.org/ns}fileid', '{DAV:}resourcetype'];
+
+ $fileInfo = $this->createMock(FileInfo::class);
+ $fileInfo->method('isReadable')->willReturn(true);
+
+ $node1 = $this->createMock(Directory::class);
+ $node2 = $this->createMock(\OCA\DAV\Connector\Sabre\File::class);
+
+ $node1->expects($this->once())
+ ->method('getInternalFileId')
+ ->willReturn('111');
+ $node1->expects($this->any())
+ ->method('getPath')
+ ->willReturn('/node1');
+ $node1->method('getFileInfo')->willReturn($fileInfo);
+ $node2->expects($this->once())
+ ->method('getInternalFileId')
+ ->willReturn('222');
+ $node2->expects($this->once())
+ ->method('getSize')
+ ->willReturn(1024);
+ $node2->expects($this->any())
+ ->method('getPath')
+ ->willReturn('/sub/node2');
+ $node2->method('getFileInfo')->willReturn($fileInfo);
+
+ $config = $this->createMock(IConfig::class);
+ $validator = $this->createMock(IFilenameValidator::class);
+ $accountManager = $this->createMock(IAccountManager::class);
+
+ $this->server->addPlugin(
+ new FilesPlugin(
+ $this->tree,
+ $config,
+ $this->createMock(IRequest::class),
+ $this->previewManager,
+ $this->createMock(IUserSession::class),
+ $validator,
+ $accountManager,
+ )
+ );
+ $this->plugin->initialize($this->server);
+ $responses = $this->plugin->prepareResponses('/files/username', $requestedProps, [$node1, $node2]);
+
+ $this->assertCount(2, $responses);
+
+ $this->assertEquals('http://example.com/owncloud/remote.php/dav/files/username/node1', $responses[0]->getHref());
+ $this->assertEquals('http://example.com/owncloud/remote.php/dav/files/username/sub/node2', $responses[1]->getHref());
+
+ $props1 = $responses[0]->getResponseProperties();
+ $this->assertEquals('111', $props1[200]['{http://owncloud.org/ns}fileid']);
+ $this->assertNull($props1[404]['{DAV:}getcontentlength']);
+ $this->assertInstanceOf('\Sabre\DAV\Xml\Property\ResourceType', $props1[200]['{DAV:}resourcetype']);
+ $resourceType1 = $props1[200]['{DAV:}resourcetype']->getValue();
+ $this->assertEquals('{DAV:}collection', $resourceType1[0]);
+
+ $props2 = $responses[1]->getResponseProperties();
+ $this->assertEquals('1024', $props2[200]['{DAV:}getcontentlength']);
+ $this->assertEquals('222', $props2[200]['{http://owncloud.org/ns}fileid']);
+ $this->assertInstanceOf('\Sabre\DAV\Xml\Property\ResourceType', $props2[200]['{DAV:}resourcetype']);
+ $this->assertCount(0, $props2[200]['{DAV:}resourcetype']->getValue());
+ }
+
+ public function testProcessFilterRulesSingle(): void {
+ $this->groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->willReturn(true);
+
+ $rules = [
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
+ ];
+
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123'])
+ ->willReturn([$tag123]);
+
+ $this->userFolder->expects($this->once())
+ ->method('searchBySystemTag')
+ ->with('OneTwoThree')
+ ->willReturn([$filesNode1, $filesNode2]);
+
+ $this->assertEquals([$filesNode1, $filesNode2], self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, 0, 0]));
+ }
+
+ public function testProcessFilterRulesAndCondition(): void {
+ $this->groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->willReturn(true);
+
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+ $filesNode3 = $this->createMock(File::class);
+ $filesNode3->expects($this->any())
+ ->method('getSize')
+ ->willReturn(14);
+ $filesNode3->expects($this->any())
+ ->method('getId')
+ ->willReturn(333);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag123, $tag456]);
+
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
+ ->willReturnMap([
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]],
+ ['FourFiveSix', 'testuser', 0, 0, [$filesNode2, $filesNode3]],
+ ]);
+
+ $rules = [
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
+ ];
+
+ $this->assertEquals([$filesNode2], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null])));
+ }
+
+ public function testProcessFilterRulesAndConditionWithOneEmptyResult(): void {
+ $this->groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->willReturn(true);
+
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag123, $tag456]);
+
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
+ ->willReturnMap([
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]],
+ ['FourFiveSix', 'testuser', 0, 0, []],
+ ]);
+
+ $rules = [
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
+ ];
+
+ $this->assertEquals([], self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]));
+ }
+
+ public function testProcessFilterRulesAndConditionWithFirstEmptyResult(): void {
+ $this->groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->willReturn(true);
+
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag123, $tag456]);
+
+ $this->userFolder->expects($this->once())
+ ->method('searchBySystemTag')
+ ->willReturnMap([
+ ['OneTwoThree', 'testuser', 0, 0, []],
+ ]);
+
+ $rules = [
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
+ ];
+
+ $this->assertEquals([], self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]));
+ }
+
+ public function testProcessFilterRulesAndConditionWithEmptyMidResult(): void {
+ $this->groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->willReturn(true);
+
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+ $filesNode3 = $this->createMock(Folder::class);
+ $filesNode3->expects($this->any())
+ ->method('getSize')
+ ->willReturn(13);
+ $filesNode3->expects($this->any())
+ ->method('getId')
+ ->willReturn(333);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag789 = $this->createMock(ISystemTag::class);
+ $tag789->expects($this->any())
+ ->method('getName')
+ ->willReturn('SevenEightNine');
+ $tag789->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456', '789'])
+ ->willReturn([$tag123, $tag456, $tag789]);
+
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
+ ->willReturnMap([
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]],
+ ['FourFiveSix', 'testuser', 0, 0, [$filesNode3]],
+ ]);
+
+ $rules = [
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '789'],
+ ];
+
+ $this->assertEquals([], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null])));
+ }
+
+ public function testProcessFilterRulesInvisibleTagAsAdmin(): void {
+ $this->groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->willReturn(true);
+
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+ $filesNode3 = $this->createMock(Folder::class);
+ $filesNode3->expects($this->any())
+ ->method('getSize')
+ ->willReturn(13);
+ $filesNode3->expects($this->any())
+ ->method('getId')
+ ->willReturn(333);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(false);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag123, $tag456]);
+
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
+ ->willReturnMap([
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]],
+ ['FourFiveSix', 'testuser', 0, 0, [$filesNode2, $filesNode3]],
+ ]);
+
+ $rules = [
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
+ ];
+
+ $this->assertEquals([$filesNode2], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null])));
+ }
+
+
+ public function testProcessFilterRulesInvisibleTagAsUser(): void {
+ $this->expectException(TagNotFoundException::class);
+
+ $this->groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->willReturn(false);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(false);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willThrowException(new TagNotFoundException());
+
+ $this->userFolder->expects($this->never())
+ ->method('searchBySystemTag');
+
+ $rules = [
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
+ ];
+
+ self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]);
+ }
+
+ public function testProcessFilterRulesVisibleTagAsUser(): void {
+ $this->groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->willReturn(false);
+
+ $tag1 = $this->createMock(ISystemTag::class);
+ $tag1->expects($this->any())
+ ->method('getId')
+ ->willReturn('123');
+ $tag1->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag1->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+
+ $tag2 = $this->createMock(ISystemTag::class);
+ $tag2->expects($this->any())
+ ->method('getId')
+ ->willReturn('123');
+ $tag2->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag2->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag1, $tag2]);
+
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode3 = $this->createMock(Folder::class);
+ $filesNode3->expects($this->any())
+ ->method('getId')
+ ->willReturn(333);
+ $filesNode3->expects($this->any())
+ ->method('getSize')
+ ->willReturn(33);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag1, $tag2]);
+
+ // main assertion: only user visible tags are being passed through.
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
+ ->willReturnMap([
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]],
+ ['FourFiveSix', 'testuser', 0, 0, [$filesNode2, $filesNode3]],
+ ]);
+
+ $rules = [
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
+ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
+ ];
+
+ $this->assertEquals([$filesNode2], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null])));
+ }
+
+ public function testProcessFavoriteFilter(): void {
+ $rules = [
+ ['name' => '{http://owncloud.org/ns}favorite', 'value' => '1'],
+ ];
+
+ $this->privateTags->expects($this->once())
+ ->method('getFavorites')
+ ->willReturn(['456', '789']);
+
+ $this->assertEquals(['456', '789'], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileIDs', [$rules])));
+ }
+
+ public static function filesBaseUriProvider(): array {
+ return [
+ ['', '', ''],
+ ['files/username', '', '/files/username'],
+ ['files/username/test', '/test', '/files/username'],
+ ['files/username/test/sub', '/test/sub', '/files/username'],
+ ['test', '/test', ''],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('filesBaseUriProvider')]
+ public function testFilesBaseUri(string $uri, string $reportPath, string $expectedUri): void {
+ $this->assertEquals($expectedUri, self::invokePrivate($this->plugin, 'getFilesBaseUri', [$uri, $reportPath]));
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php b/apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php
new file mode 100644
index 00000000000..bc1d50ac41f
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\MaintenancePlugin;
+use OCP\IConfig;
+use OCP\IL10N;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+/**
+ * Class MaintenancePluginTest
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre
+ */
+class MaintenancePluginTest extends TestCase {
+ private IConfig&MockObject $config;
+ private IL10N&MockObject $l10n;
+ private MaintenancePlugin $maintenancePlugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->maintenancePlugin = new MaintenancePlugin($this->config, $this->l10n);
+ }
+
+
+ public function testMaintenanceMode(): void {
+ $this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class);
+ $this->expectExceptionMessage('System is in maintenance mode.');
+
+ $this->config
+ ->expects($this->exactly(1))
+ ->method('getSystemValueBool')
+ ->with('maintenance')
+ ->willReturn(true);
+ $this->l10n
+ ->expects($this->any())
+ ->method('t')
+ ->willReturnArgument(0);
+
+ $this->maintenancePlugin->checkMaintenanceMode();
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/NodeTest.php b/apps/dav/tests/unit/Connector/Sabre/NodeTest.php
new file mode 100644
index 00000000000..11970769a1e
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/NodeTest.php
@@ -0,0 +1,271 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\Files\FileInfo;
+use OC\Files\Mount\MountPoint;
+use OC\Files\Node\Folder;
+use OC\Files\View;
+use OC\Share20\ShareAttributes;
+use OCA\DAV\Connector\Sabre\File;
+use OCA\Files_Sharing\SharedMount;
+use OCA\Files_Sharing\SharedStorage;
+use OCP\Constants;
+use OCP\Files\Cache\ICacheEntry;
+use OCP\Files\Mount\IMountPoint;
+use OCP\Files\Storage\IStorage;
+use OCP\ICache;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
+
+/**
+ * Class NodeTest
+ *
+ * @group DB
+ * @package OCA\DAV\Tests\unit\Connector\Sabre
+ */
+class NodeTest extends \Test\TestCase {
+ public static function davPermissionsProvider(): array {
+ return [
+ [Constants::PERMISSION_ALL, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNVW'],
+ [Constants::PERMISSION_ALL, 'dir', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNVCK'],
+ [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL, false, 'test', 'SRGDNVW'],
+ [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL, true, 'test', 'SRMGDNVW'],
+ [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL, true, '' , 'SRMGDNVW'],
+ [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE, true, '' , 'SRMGDNV'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, 'file', true, Constants::PERMISSION_ALL, false, 'test', 'SGDNVW'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGD'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGNVW'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNVW'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_READ, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RDNVW'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, 'dir', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNV'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_READ, 'dir', false, Constants::PERMISSION_ALL, false, 'test', 'RDNVCK'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('davPermissionsProvider')]
+ public function testDavPermissions(int $permissions, string $type, bool $shared, int $shareRootPermissions, bool $mounted, string $internalPath, string $expected): void {
+ $info = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getPermissions', 'isShared', 'isMounted', 'getType', 'getInternalPath', 'getStorage', 'getMountPoint'])
+ ->getMock();
+ $info->method('getPermissions')
+ ->willReturn($permissions);
+ $info->method('isShared')
+ ->willReturn($shared);
+ $info->method('isMounted')
+ ->willReturn($mounted);
+ $info->method('getType')
+ ->willReturn($type);
+ $info->method('getInternalPath')
+ ->willReturn($internalPath);
+ $info->method('getMountPoint')
+ ->willReturnCallback(function () use ($shared) {
+ if ($shared) {
+ return $this->createMock(SharedMount::class);
+ } else {
+ return $this->createMock(MountPoint::class);
+ }
+ });
+ $storage = $this->createMock(IStorage::class);
+ if ($shared) {
+ $storage->method('instanceOfStorage')
+ ->willReturn(true);
+ $cache = $this->createMock(ICache::class);
+ $storage->method('getCache')
+ ->willReturn($cache);
+ $shareRootEntry = $this->createMock(ICacheEntry::class);
+ $cache->method('get')
+ ->willReturn($shareRootEntry);
+ $shareRootEntry->method('getPermissions')
+ ->willReturn($shareRootPermissions);
+ } else {
+ $storage->method('instanceOfStorage')
+ ->willReturn(false);
+ }
+ $info->method('getStorage')
+ ->willReturn($storage);
+ $view = $this->createMock(View::class);
+
+ $node = new File($view, $info);
+ $this->assertEquals($expected, $node->getDavPermissions());
+ }
+
+ public static function sharePermissionsProvider(): array {
+ return [
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 1, 1],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 3, 3],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 5, 1],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 7, 3],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 9, 1],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 11, 3],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 13, 1],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 15, 3],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 17, 17],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 19, 19],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 21, 17],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 23, 19],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 25, 17],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 27, 19],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 29, 17],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 30, 18],
+ [\OCP\Files\FileInfo::TYPE_FILE, null, 31, 19],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 1, 1],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 3, 3],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 5, 5],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 7, 7],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 9, 9],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 11, 11],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 13, 13],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 15, 15],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 17, 17],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 19, 19],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 21, 21],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 23, 23],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 25, 25],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 27, 27],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 29, 29],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 30, 30],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, null, 31, 31],
+ [\OCP\Files\FileInfo::TYPE_FOLDER, 'shareToken', 7, 7],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('sharePermissionsProvider')]
+ public function testSharePermissions(string $type, ?string $user, int $permissions, int $expected): void {
+ $storage = $this->createMock(IStorage::class);
+ $storage->method('getPermissions')->willReturn($permissions);
+
+ $mountpoint = $this->createMock(IMountPoint::class);
+ $mountpoint->method('getMountPoint')->willReturn('myPath');
+ $shareManager = $this->createMock(IManager::class);
+ $share = $this->createMock(IShare::class);
+
+ if ($user === null) {
+ $shareManager->expects($this->never())->method('getShareByToken');
+ $share->expects($this->never())->method('getPermissions');
+ } else {
+ $shareManager->expects($this->once())->method('getShareByToken')->with($user)
+ ->willReturn($share);
+ $share->expects($this->once())->method('getPermissions')->willReturn($permissions);
+ }
+
+ $info = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getStorage', 'getType', 'getMountPoint', 'getPermissions'])
+ ->getMock();
+
+ $info->method('getStorage')->willReturn($storage);
+ $info->method('getType')->willReturn($type);
+ $info->method('getMountPoint')->willReturn($mountpoint);
+ $info->method('getPermissions')->willReturn($permissions);
+
+ $view = $this->createMock(View::class);
+
+ $node = new File($view, $info);
+ $this->invokePrivate($node, 'shareManager', [$shareManager]);
+ $this->assertEquals($expected, $node->getSharePermissions($user));
+ }
+
+ public function testShareAttributes(): void {
+ $storage = $this->getMockBuilder(SharedStorage::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getShare'])
+ ->getMock();
+
+ $shareManager = $this->createMock(IManager::class);
+ $share = $this->createMock(IShare::class);
+
+ $storage->expects($this->once())
+ ->method('getShare')
+ ->willReturn($share);
+
+ $attributes = new ShareAttributes();
+ $attributes->setAttribute('permissions', 'download', false);
+
+ $share->expects($this->once())->method('getAttributes')->willReturn($attributes);
+
+ /** @var Folder&MockObject $info */
+ $info = $this->getMockBuilder(Folder::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getStorage', 'getType'])
+ ->getMock();
+
+ $info->method('getStorage')->willReturn($storage);
+ $info->method('getType')->willReturn(FileInfo::TYPE_FOLDER);
+
+ /** @var View&MockObject $view */
+ $view = $this->createMock(View::class);
+
+ $node = new File($view, $info);
+ $this->invokePrivate($node, 'shareManager', [$shareManager]);
+ $this->assertEquals($attributes->toArray(), $node->getShareAttributes());
+ }
+
+ public function testShareAttributesNonShare(): void {
+ $storage = $this->createMock(IStorage::class);
+ $shareManager = $this->createMock(IManager::class);
+
+ /** @var Folder&MockObject */
+ $info = $this->getMockBuilder(Folder::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getStorage', 'getType'])
+ ->getMock();
+
+ $info->method('getStorage')->willReturn($storage);
+ $info->method('getType')->willReturn(FileInfo::TYPE_FOLDER);
+
+ /** @var View&MockObject */
+ $view = $this->createMock(View::class);
+
+ $node = new File($view, $info);
+ $this->invokePrivate($node, 'shareManager', [$shareManager]);
+ $this->assertEquals([], $node->getShareAttributes());
+ }
+
+ public static function sanitizeMtimeProvider(): array {
+ return [
+ [123456789, 123456789],
+ ['987654321', 987654321],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('sanitizeMtimeProvider')]
+ public function testSanitizeMtime(string|int $mtime, int $expected): void {
+ $view = $this->getMockBuilder(View::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $info = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $node = new File($view, $info);
+ $result = $this->invokePrivate($node, 'sanitizeMtime', [$mtime]);
+ $this->assertEquals($expected, $result);
+ }
+
+ public static function invalidSanitizeMtimeProvider(): array {
+ return [
+ [-1337], [0], ['abcdef'], ['-1337'], ['0'], [12321], [24 * 60 * 60 - 1],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('invalidSanitizeMtimeProvider')]
+ public function testInvalidSanitizeMtime(int|string $mtime): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $view = $this->createMock(View::class);
+ $info = $this->createMock(FileInfo::class);
+
+ $node = new File($view, $info);
+ self::invokePrivate($node, 'sanitizeMtime', [$mtime]);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php
new file mode 100644
index 00000000000..b07778e4fbd
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php
@@ -0,0 +1,243 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\Files\FileInfo;
+use OC\Files\Filesystem;
+use OC\Files\Mount\Manager;
+use OC\Files\Storage\Common;
+use OC\Files\Storage\Temporary;
+use OC\Files\View;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\DAV\Connector\Sabre\File;
+use OCA\DAV\Connector\Sabre\ObjectTree;
+use OCP\Files\Mount\IMountManager;
+
+/**
+ * Class ObjectTreeTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit\Connector\Sabre
+ */
+class ObjectTreeTest extends \Test\TestCase {
+ public static function copyDataProvider(): array {
+ return [
+ // copy into same dir
+ ['a', 'b', ''],
+ // copy into same dir
+ ['a/a', 'a/b', 'a'],
+ // copy into another dir
+ ['a', 'sub/a', 'sub'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('copyDataProvider')]
+ public function testCopy(string $sourcePath, string $targetPath, string $targetParent): void {
+ $view = $this->createMock(View::class);
+ $view->expects($this->once())
+ ->method('verifyPath')
+ ->with($targetParent);
+ $view->expects($this->once())
+ ->method('file_exists')
+ ->with($targetPath)
+ ->willReturn(false);
+ $view->expects($this->once())
+ ->method('copy')
+ ->with($sourcePath, $targetPath)
+ ->willReturn(true);
+
+ $info = $this->createMock(FileInfo::class);
+ $info->expects($this->once())
+ ->method('isCreatable')
+ ->willReturn(true);
+
+ $view->expects($this->once())
+ ->method('getFileInfo')
+ ->with($targetParent === '' ? '.' : $targetParent)
+ ->willReturn($info);
+
+ $rootDir = new Directory($view, $info);
+ $objectTree = $this->getMockBuilder(ObjectTree::class)
+ ->onlyMethods(['nodeExists', 'getNodeForPath'])
+ ->setConstructorArgs([$rootDir, $view])
+ ->getMock();
+
+ $objectTree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with($this->identicalTo($sourcePath))
+ ->willReturn(false);
+
+ /** @var ObjectTree $objectTree */
+ $mountManager = Filesystem::getMountManager();
+ $objectTree->init($rootDir, $view, $mountManager);
+ $objectTree->copy($sourcePath, $targetPath);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('copyDataProvider')]
+ public function testCopyFailNotCreatable($sourcePath, $targetPath, $targetParent): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $view = $this->createMock(View::class);
+ $view->expects($this->never())
+ ->method('verifyPath');
+ $view->expects($this->once())
+ ->method('file_exists')
+ ->with($targetPath)
+ ->willReturn(false);
+ $view->expects($this->never())
+ ->method('copy');
+
+ $info = $this->createMock(FileInfo::class);
+ $info->expects($this->once())
+ ->method('isCreatable')
+ ->willReturn(false);
+
+ $view->expects($this->once())
+ ->method('getFileInfo')
+ ->with($targetParent === '' ? '.' : $targetParent)
+ ->willReturn($info);
+
+ $rootDir = new Directory($view, $info);
+ $objectTree = $this->getMockBuilder(ObjectTree::class)
+ ->onlyMethods(['nodeExists', 'getNodeForPath'])
+ ->setConstructorArgs([$rootDir, $view])
+ ->getMock();
+
+ $objectTree->expects($this->never())
+ ->method('getNodeForPath');
+
+ /** @var ObjectTree $objectTree */
+ $mountManager = Filesystem::getMountManager();
+ $objectTree->init($rootDir, $view, $mountManager);
+ $objectTree->copy($sourcePath, $targetPath);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('nodeForPathProvider')]
+ public function testGetNodeForPath(
+ string $inputFileName,
+ string $fileInfoQueryPath,
+ string $outputFileName,
+ string $type,
+ ): void {
+ $rootNode = $this->createMock(Directory::class);
+ $mountManager = $this->createMock(Manager::class);
+ $view = $this->createMock(View::class);
+ $fileInfo = $this->createMock(FileInfo::class);
+ $fileInfo->method('getType')
+ ->willReturn($type);
+ $fileInfo->method('getName')
+ ->willReturn($outputFileName);
+ $fileInfo->method('getStorage')
+ ->willReturn($this->createMock(Common::class));
+
+ $view->method('getFileInfo')
+ ->with($fileInfoQueryPath)
+ ->willReturn($fileInfo);
+
+ $tree = new ObjectTree();
+ $tree->init($rootNode, $view, $mountManager);
+
+ $node = $tree->getNodeForPath($inputFileName);
+
+ $this->assertNotNull($node);
+ $this->assertEquals($outputFileName, $node->getName());
+
+ if ($type === 'file') {
+ $this->assertInstanceOf(File::class, $node);
+ } else {
+ $this->assertInstanceOf(Directory::class, $node);
+ }
+ }
+
+ public static function nodeForPathProvider(): array {
+ return [
+ // regular file
+ [
+ 'regularfile.txt',
+ 'regularfile.txt',
+ 'regularfile.txt',
+ 'file',
+ ],
+ // regular directory
+ [
+ 'regulardir',
+ 'regulardir',
+ 'regulardir',
+ 'dir',
+ ],
+ // regular file in subdir
+ [
+ 'subdir/regularfile.txt',
+ 'subdir/regularfile.txt',
+ 'regularfile.txt',
+ 'file',
+ ],
+ // regular directory in subdir
+ [
+ 'subdir/regulardir',
+ 'subdir/regulardir',
+ 'regulardir',
+ 'dir',
+ ],
+ ];
+ }
+
+
+ public function testGetNodeForPathInvalidPath(): void {
+ $this->expectException(InvalidPath::class);
+
+ $path = '/foo\bar';
+
+
+ $storage = new Temporary([]);
+
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['resolvePath'])
+ ->getMock();
+ $view->expects($this->once())
+ ->method('resolvePath')
+ ->willReturnCallback(function ($path) use ($storage) {
+ return [$storage, ltrim($path, '/')];
+ });
+
+ $rootNode = $this->createMock(Directory::class);
+ $mountManager = $this->createMock(IMountManager::class);
+
+ $tree = new ObjectTree();
+ $tree->init($rootNode, $view, $mountManager);
+
+ $tree->getNodeForPath($path);
+ }
+
+ public function testGetNodeForPathRoot(): void {
+ $path = '/';
+
+
+ $storage = new Temporary([]);
+
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['resolvePath'])
+ ->getMock();
+ $view->expects($this->any())
+ ->method('resolvePath')
+ ->willReturnCallback(function ($path) use ($storage) {
+ return [$storage, ltrim($path, '/')];
+ });
+
+ $rootNode = $this->createMock(Directory::class);
+ $mountManager = $this->createMock(IMountManager::class);
+
+ $tree = new ObjectTree();
+ $tree->init($rootNode, $view, $mountManager);
+
+ $this->assertInstanceOf('\Sabre\DAV\INode', $tree->getNodeForPath($path));
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php b/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php
new file mode 100644
index 00000000000..e32d2671063
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php
@@ -0,0 +1,937 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\KnownUser\KnownUserService;
+use OC\User\User;
+use OCA\DAV\CalDAV\Proxy\Proxy;
+use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCP\Accounts\IAccount;
+use OCP\Accounts\IAccountManager;
+use OCP\Accounts\IAccountProperty;
+use OCP\Accounts\IAccountPropertyCollection;
+use OCP\App\IAppManager;
+use OCP\IConfig;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Share\IManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception;
+use Sabre\DAV\PropPatch;
+use Test\TestCase;
+
+class PrincipalTest extends TestCase {
+ private IUserManager&MockObject $userManager;
+ private IGroupManager&MockObject $groupManager;
+ private IAccountManager&MockObject $accountManager;
+ private IManager&MockObject $shareManager;
+ private IUserSession&MockObject $userSession;
+ private IAppManager&MockObject $appManager;
+ private ProxyMapper&MockObject $proxyMapper;
+ private KnownUserService&MockObject $knownUserService;
+ private IConfig&MockObject $config;
+ private IFactory&MockObject $languageFactory;
+ private Principal $connector;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->accountManager = $this->createMock(IAccountManager::class);
+ $this->shareManager = $this->createMock(IManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->proxyMapper = $this->createMock(ProxyMapper::class);
+ $this->knownUserService = $this->createMock(KnownUserService::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->languageFactory = $this->createMock(IFactory::class);
+
+ $this->connector = new Principal(
+ $this->userManager,
+ $this->groupManager,
+ $this->accountManager,
+ $this->shareManager,
+ $this->userSession,
+ $this->appManager,
+ $this->proxyMapper,
+ $this->knownUserService,
+ $this->config,
+ $this->languageFactory
+ );
+ }
+
+ public function testGetPrincipalsByPrefixWithoutPrefix(): void {
+ $response = $this->connector->getPrincipalsByPrefix('');
+ $this->assertSame([], $response);
+ }
+
+ public function testGetPrincipalsByPrefixWithUsers(): void {
+ $fooUser = $this->createMock(User::class);
+ $fooUser
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('foo');
+ $fooUser
+ ->expects($this->once())
+ ->method('getDisplayName')
+ ->willReturn('Dr. Foo-Bar');
+ $fooUser
+ ->expects($this->once())
+ ->method('getSystemEMailAddress')
+ ->willReturn('');
+ $barUser = $this->createMock(User::class);
+ $barUser
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('bar');
+ $barUser
+ ->expects($this->once())
+ ->method('getSystemEMailAddress')
+ ->willReturn('bar@nextcloud.com');
+ $this->userManager
+ ->expects($this->once())
+ ->method('search')
+ ->with('')
+ ->willReturn([$fooUser, $barUser]);
+
+ $this->languageFactory
+ ->expects($this->exactly(2))
+ ->method('getUserLanguage')
+ ->willReturnMap([
+ [$fooUser, 'de'],
+ [$barUser, 'en'],
+ ]);
+
+ $fooAccountPropertyCollection = $this->createMock(IAccountPropertyCollection::class);
+ $fooAccountPropertyCollection->expects($this->once())
+ ->method('getProperties')
+ ->willReturn([]);
+ $fooAccount = $this->createMock(IAccount::class);
+ $fooAccount->expects($this->once())
+ ->method('getPropertyCollection')
+ ->with(IAccountManager::COLLECTION_EMAIL)
+ ->willReturn($fooAccountPropertyCollection);
+
+ $emailPropertyOne = $this->createMock(IAccountProperty::class);
+ $emailPropertyOne->expects($this->once())
+ ->method('getValue')
+ ->willReturn('alias@nextcloud.com');
+ $emailPropertyTwo = $this->createMock(IAccountProperty::class);
+ $emailPropertyTwo->expects($this->once())
+ ->method('getValue')
+ ->willReturn('alias2@nextcloud.com');
+
+ $barAccountPropertyCollection = $this->createMock(IAccountPropertyCollection::class);
+ $barAccountPropertyCollection->expects($this->once())
+ ->method('getProperties')
+ ->willReturn([$emailPropertyOne, $emailPropertyTwo]);
+ $barAccount = $this->createMock(IAccount::class);
+ $barAccount->expects($this->once())
+ ->method('getPropertyCollection')
+ ->with(IAccountManager::COLLECTION_EMAIL)
+ ->willReturn($barAccountPropertyCollection);
+
+ $this->accountManager
+ ->expects($this->exactly(2))
+ ->method('getAccount')
+ ->willReturnMap([
+ [$fooUser, $fooAccount],
+ [$barUser, $barAccount],
+ ]);
+
+ $expectedResponse = [
+ 0 => [
+ 'uri' => 'principals/users/foo',
+ '{DAV:}displayname' => 'Dr. Foo-Bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
+ '{http://nextcloud.com/ns}language' => 'de',
+ ],
+ 1 => [
+ 'uri' => 'principals/users/bar',
+ '{DAV:}displayname' => 'bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
+ '{http://nextcloud.com/ns}language' => 'en',
+ '{http://sabredav.org/ns}email-address' => 'bar@nextcloud.com',
+ '{DAV:}alternate-URI-set' => ['mailto:alias@nextcloud.com', 'mailto:alias2@nextcloud.com']
+ ]
+ ];
+ $response = $this->connector->getPrincipalsByPrefix('principals/users');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetPrincipalsByPrefixEmpty(): void {
+ $this->userManager
+ ->expects($this->once())
+ ->method('search')
+ ->with('')
+ ->willReturn([]);
+
+ $response = $this->connector->getPrincipalsByPrefix('principals/users');
+ $this->assertSame([], $response);
+ }
+
+ public function testGetPrincipalsByPathWithoutMail(): void {
+ $fooUser = $this->createMock(User::class);
+ $fooUser
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('foo');
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willReturn($fooUser);
+
+ $this->languageFactory
+ ->expects($this->once())
+ ->method('getUserLanguage')
+ ->with($fooUser)
+ ->willReturn('de');
+
+ $expectedResponse = [
+ 'uri' => 'principals/users/foo',
+ '{DAV:}displayname' => 'foo',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
+ '{http://nextcloud.com/ns}language' => 'de'
+ ];
+ $response = $this->connector->getPrincipalByPath('principals/users/foo');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetPrincipalsByPathWithMail(): void {
+ $fooUser = $this->createMock(User::class);
+ $fooUser
+ ->expects($this->once())
+ ->method('getSystemEMailAddress')
+ ->willReturn('foo@nextcloud.com');
+ $fooUser
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('foo');
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willReturn($fooUser);
+
+ $this->languageFactory
+ ->expects($this->once())
+ ->method('getUserLanguage')
+ ->with($fooUser)
+ ->willReturn('de');
+
+ $expectedResponse = [
+ 'uri' => 'principals/users/foo',
+ '{DAV:}displayname' => 'foo',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
+ '{http://nextcloud.com/ns}language' => 'de',
+ '{http://sabredav.org/ns}email-address' => 'foo@nextcloud.com',
+ ];
+ $response = $this->connector->getPrincipalByPath('principals/users/foo');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetPrincipalsByPathEmpty(): void {
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willReturn(null);
+
+ $response = $this->connector->getPrincipalByPath('principals/users/foo');
+ $this->assertNull($response);
+ }
+
+ public function testGetGroupMemberSet(): void {
+ $response = $this->connector->getGroupMemberSet('principals/users/foo');
+ $this->assertSame([], $response);
+ }
+
+
+ public function testGetGroupMemberSetEmpty(): void {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Principal not found');
+
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willReturn(null);
+
+ $this->connector->getGroupMemberSet('principals/users/foo/calendar-proxy-read');
+ }
+
+ public function testGetGroupMemberSetProxyRead(): void {
+ $fooUser = $this->createMock(User::class);
+ $fooUser
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('foo');
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willReturn($fooUser);
+
+ $proxy1 = new Proxy();
+ $proxy1->setProxyId('proxyId1');
+ $proxy1->setPermissions(1);
+
+ $proxy2 = new Proxy();
+ $proxy2->setProxyId('proxyId2');
+ $proxy2->setPermissions(3);
+
+ $proxy3 = new Proxy();
+ $proxy3->setProxyId('proxyId3');
+ $proxy3->setPermissions(3);
+
+ $this->proxyMapper->expects($this->once())
+ ->method('getProxiesOf')
+ ->with('principals/users/foo')
+ ->willReturn([$proxy1, $proxy2, $proxy3]);
+
+ $this->assertEquals(['proxyId1'], $this->connector->getGroupMemberSet('principals/users/foo/calendar-proxy-read'));
+ }
+
+ public function testGetGroupMemberSetProxyWrite(): void {
+ $fooUser = $this->createMock(User::class);
+ $fooUser
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('foo');
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willReturn($fooUser);
+
+ $proxy1 = new Proxy();
+ $proxy1->setProxyId('proxyId1');
+ $proxy1->setPermissions(1);
+
+ $proxy2 = new Proxy();
+ $proxy2->setProxyId('proxyId2');
+ $proxy2->setPermissions(3);
+
+ $proxy3 = new Proxy();
+ $proxy3->setProxyId('proxyId3');
+ $proxy3->setPermissions(3);
+
+ $this->proxyMapper->expects($this->once())
+ ->method('getProxiesOf')
+ ->with('principals/users/foo')
+ ->willReturn([$proxy1, $proxy2, $proxy3]);
+
+ $this->assertEquals(['proxyId2', 'proxyId3'], $this->connector->getGroupMemberSet('principals/users/foo/calendar-proxy-write'));
+ }
+
+ public function testGetGroupMembership(): void {
+ $fooUser = $this->createMock(User::class);
+ $group1 = $this->createMock(IGroup::class);
+ $group1->expects($this->once())
+ ->method('getGID')
+ ->willReturn('group1');
+ $group2 = $this->createMock(IGroup::class);
+ $group2->expects($this->once())
+ ->method('getGID')
+ ->willReturn('foo/bar');
+ $this->userManager
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->with('foo')
+ ->willReturn($fooUser);
+ $this->groupManager
+ ->expects($this->once())
+ ->method('getUserGroups')
+ ->with($fooUser)
+ ->willReturn([
+ $group1,
+ $group2,
+ ]);
+
+ $proxy1 = new Proxy();
+ $proxy1->setOwnerId('proxyId1');
+ $proxy1->setPermissions(1);
+
+ $proxy2 = new Proxy();
+ $proxy2->setOwnerId('proxyId2');
+ $proxy2->setPermissions(3);
+
+ $this->proxyMapper->expects($this->once())
+ ->method('getProxiesFor')
+ ->with('principals/users/foo')
+ ->willReturn([$proxy1, $proxy2]);
+
+ $expectedResponse = [
+ 'principals/groups/group1',
+ 'principals/groups/foo%2Fbar',
+ 'proxyId1/calendar-proxy-read',
+ 'proxyId2/calendar-proxy-write',
+ ];
+ $response = $this->connector->getGroupMembership('principals/users/foo');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+
+ public function testGetGroupMembershipEmpty(): void {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Principal not found');
+
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willReturn(null);
+
+ $this->connector->getGroupMembership('principals/users/foo');
+ }
+
+
+ public function testSetGroupMembership(): void {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Setting members of the group is not supported yet');
+
+ $this->connector->setGroupMemberSet('principals/users/foo', ['foo']);
+ }
+
+ public function testSetGroupMembershipProxy(): void {
+ $fooUser = $this->createMock(User::class);
+ $fooUser
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('foo');
+ $barUser = $this->createMock(User::class);
+ $barUser
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('bar');
+ $this->userManager
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->willReturnMap([
+ ['foo', $fooUser],
+ ['bar', $barUser],
+ ]);
+
+ $this->proxyMapper->expects($this->once())
+ ->method('getProxiesOf')
+ ->with('principals/users/foo')
+ ->willReturn([]);
+
+ $this->proxyMapper->expects($this->once())
+ ->method('insert')
+ ->with($this->callback(function ($proxy) {
+ /** @var Proxy $proxy */
+ if ($proxy->getOwnerId() !== 'principals/users/foo') {
+ return false;
+ }
+ if ($proxy->getProxyId() !== 'principals/users/bar') {
+ return false;
+ }
+ if ($proxy->getPermissions() !== 3) {
+ return false;
+ }
+
+ return true;
+ }));
+
+ $this->connector->setGroupMemberSet('principals/users/foo/calendar-proxy-write', ['principals/users/bar']);
+ }
+
+ public function testUpdatePrincipal(): void {
+ $this->assertSame(0, $this->connector->updatePrincipal('foo', new PropPatch([])));
+ }
+
+ public function testSearchPrincipalsWithEmptySearchProperties(): void {
+ $this->assertSame([], $this->connector->searchPrincipals('principals/users', []));
+ }
+
+ public function testSearchPrincipalsWithWrongPrefixPath(): void {
+ $this->assertSame([], $this->connector->searchPrincipals('principals/groups',
+ ['{http://sabredav.org/ns}email-address' => 'foo']));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('searchPrincipalsDataProvider')]
+ public function testSearchPrincipals(bool $sharingEnabled, bool $groupsOnly, string $test, array $result): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareAPIEnabled')
+ ->willReturn($sharingEnabled);
+
+ $getUserGroupIdsReturnMap = [];
+
+ if ($sharingEnabled) {
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumeration')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn($groupsOnly);
+
+ if ($groupsOnly) {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->atLeastOnce())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $getUserGroupIdsReturnMap[] = [$user, ['group1', 'group2', 'group5']];
+ }
+ } else {
+ $this->config->expects($this->never())
+ ->method('getAppValue');
+ $this->shareManager->expects($this->never())
+ ->method('shareWithGroupMembersOnly');
+ $this->groupManager->expects($this->never())
+ ->method($this->anything());
+ }
+
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')->willReturn('user2');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')->willReturn('user3');
+ $user4 = $this->createMock(IUser::class);
+ $user4->method('getUID')->willReturn('user4');
+
+ if ($sharingEnabled) {
+ $this->userManager->expects($this->once())
+ ->method('getByEmail')
+ ->with('user@example.com')
+ ->willReturn([$user2, $user3]);
+
+ $this->userManager->expects($this->once())
+ ->method('searchDisplayName')
+ ->with('User 12')
+ ->willReturn([$user3, $user4]);
+ } else {
+ $this->userManager->expects($this->never())
+ ->method('getByEmail');
+
+ $this->userManager->expects($this->never())
+ ->method('searchDisplayName');
+ }
+
+ if ($sharingEnabled && $groupsOnly) {
+ $getUserGroupIdsReturnMap[] = [$user2, ['group1', 'group3']];
+ $getUserGroupIdsReturnMap[] = [$user3, ['group3', 'group4']];
+ $getUserGroupIdsReturnMap[] = [$user4, ['group4', 'group5']];
+ }
+
+ $this->groupManager->expects($this->any())
+ ->method('getUserGroupIds')
+ ->willReturnMap($getUserGroupIdsReturnMap);
+
+
+ $this->assertEquals($result, $this->connector->searchPrincipals('principals/users',
+ ['{http://sabredav.org/ns}email-address' => 'user@example.com',
+ '{DAV:}displayname' => 'User 12'], $test));
+ }
+
+ public static function searchPrincipalsDataProvider(): array {
+ return [
+ [true, false, 'allof', ['principals/users/user3']],
+ [true, false, 'anyof', ['principals/users/user2', 'principals/users/user3', 'principals/users/user4']],
+ [true, true, 'allof', []],
+ [true, true, 'anyof', ['principals/users/user2', 'principals/users/user4']],
+ [false, false, 'allof', []],
+ [false, false, 'anyof', []],
+ ];
+ }
+
+ public function testSearchPrincipalByCalendarUserAddressSet(): void {
+ $this->shareManager->expects($this->exactly(2))
+ ->method('shareAPIEnabled')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->exactly(2))
+ ->method('allowEnumeration')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->exactly(2))
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(false);
+
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')->willReturn('user2');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')->willReturn('user3');
+
+ $this->userManager->expects($this->once())
+ ->method('getByEmail')
+ ->with('user@example.com')
+ ->willReturn([$user2, $user3]);
+
+ $this->assertEquals([
+ 'principals/users/user2',
+ 'principals/users/user3',
+ ], $this->connector->searchPrincipals('principals/users',
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => 'user@example.com']));
+ }
+
+ public function testSearchPrincipalWithEnumerationDisabledDisplayname(): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareAPIEnabled')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumeration')
+ ->willReturn(false);
+
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(false);
+
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumerationFullMatch')
+ ->willReturn(true);
+
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')->willReturn('user2');
+ $user2->method('getDisplayName')->willReturn('User 2');
+ $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')->willReturn('user3');
+ $user3->method('getDisplayName')->willReturn('User 22');
+ $user3->method('getSystemEMailAddress')->willReturn('user2@foo.bar123');
+ $user4 = $this->createMock(IUser::class);
+ $user4->method('getUID')->willReturn('user4');
+ $user4->method('getDisplayName')->willReturn('User 222');
+ $user4->method('getSystemEMailAddress')->willReturn('user2@foo.bar456');
+
+ $this->userManager->expects($this->once())
+ ->method('searchDisplayName')
+ ->with('User 2')
+ ->willReturn([$user2, $user3, $user4]);
+
+ $this->assertEquals(['principals/users/user2'], $this->connector->searchPrincipals('principals/users',
+ ['{DAV:}displayname' => 'User 2']));
+ }
+
+ public function testSearchPrincipalWithEnumerationDisabledDisplaynameOnFullMatch(): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareAPIEnabled')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumeration')
+ ->willReturn(false);
+
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(false);
+
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumerationFullMatch')
+ ->willReturn(false);
+
+ $this->assertEquals([], $this->connector->searchPrincipals('principals/users',
+ ['{DAV:}displayname' => 'User 2']));
+ }
+
+ public function testSearchPrincipalWithEnumerationDisabledEmail(): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareAPIEnabled')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumeration')
+ ->willReturn(false);
+
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(false);
+
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumerationFullMatch')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('matchEmail')
+ ->willReturn(true);
+
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')->willReturn('user2');
+ $user2->method('getDisplayName')->willReturn('User 2');
+ $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')->willReturn('user3');
+ $user2->method('getDisplayName')->willReturn('User 22');
+ $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar123');
+ $user4 = $this->createMock(IUser::class);
+ $user4->method('getUID')->willReturn('user4');
+ $user2->method('getDisplayName')->willReturn('User 222');
+ $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar456');
+
+ $this->userManager->expects($this->once())
+ ->method('getByEmail')
+ ->with('user2@foo.bar')
+ ->willReturn([$user2]);
+
+ $this->assertEquals(['principals/users/user2'], $this->connector->searchPrincipals('principals/users',
+ ['{http://sabredav.org/ns}email-address' => 'user2@foo.bar']));
+ }
+
+ public function testSearchPrincipalWithEnumerationDisabledEmailOnFullMatch(): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareAPIEnabled')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumeration')
+ ->willReturn(false);
+
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(false);
+
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumerationFullMatch')
+ ->willReturn(false);
+
+
+ $this->assertEquals([], $this->connector->searchPrincipals('principals/users',
+ ['{http://sabredav.org/ns}email-address' => 'user2@foo.bar']));
+ }
+
+ public function testSearchPrincipalWithEnumerationLimitedDisplayname(): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareAPIEnabled')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumeration')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('limitEnumerationToGroups')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(false);
+
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')->willReturn('user2');
+ $user2->method('getDisplayName')->willReturn('User 2');
+ $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')->willReturn('user3');
+ $user3->method('getDisplayName')->willReturn('User 22');
+ $user3->method('getSystemEMailAddress')->willReturn('user2@foo.bar123');
+ $user4 = $this->createMock(IUser::class);
+ $user4->method('getUID')->willReturn('user4');
+ $user4->method('getDisplayName')->willReturn('User 222');
+ $user4->method('getSystemEMailAddress')->willReturn('user2@foo.bar456');
+
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user2);
+
+ $this->groupManager->expects($this->exactly(4))
+ ->method('getUserGroupIds')
+ ->willReturnMap([
+ [$user2, ['group1']],
+ [$user3, ['group1']],
+ [$user4, ['group2']],
+ ]);
+
+ $this->userManager->expects($this->once())
+ ->method('searchDisplayName')
+ ->with('User')
+ ->willReturn([$user2, $user3, $user4]);
+
+
+ $this->assertEquals([
+ 'principals/users/user2',
+ 'principals/users/user3',
+ ], $this->connector->searchPrincipals('principals/users',
+ ['{DAV:}displayname' => 'User']));
+ }
+
+ public function testSearchPrincipalWithEnumerationLimitedMail(): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareAPIEnabled')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('allowEnumeration')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('limitEnumerationToGroups')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(false);
+
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')->willReturn('user2');
+ $user2->method('getDisplayName')->willReturn('User 2');
+ $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')->willReturn('user3');
+ $user3->method('getDisplayName')->willReturn('User 22');
+ $user3->method('getSystemEMailAddress')->willReturn('user2@foo.bar123');
+ $user4 = $this->createMock(IUser::class);
+ $user4->method('getUID')->willReturn('user4');
+ $user4->method('getDisplayName')->willReturn('User 222');
+ $user4->method('getSystemEMailAddress')->willReturn('user2@foo.bar456');
+
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user2);
+
+ $this->groupManager->expects($this->exactly(4))
+ ->method('getUserGroupIds')
+ ->willReturnMap([
+ [$user2, ['group1']],
+ [$user3, ['group1']],
+ [$user4, ['group2']],
+ ]);
+
+ $this->userManager->expects($this->once())
+ ->method('getByEmail')
+ ->with('user')
+ ->willReturn([$user2, $user3, $user4]);
+
+
+ $this->assertEquals([
+ 'principals/users/user2',
+ 'principals/users/user3'
+ ], $this->connector->searchPrincipals('principals/users',
+ ['{http://sabredav.org/ns}email-address' => 'user']));
+ }
+
+ public function testFindByUriSharingApiDisabled(): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareApiEnabled')
+ ->willReturn(false);
+
+ $this->assertEquals(null, $this->connector->findByUri('mailto:user@foo.com', 'principals/users'));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('findByUriWithGroupRestrictionDataProvider')]
+ public function testFindByUriWithGroupRestriction(string $uri, string $email, ?string $expects): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareApiEnabled')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(true);
+
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')->willReturn('user2');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')->willReturn('user3');
+
+ $this->userManager->expects($this->once())
+ ->method('getByEmail')
+ ->with($email)
+ ->willReturn([$email === 'user2@foo.bar' ? $user2 : $user3]);
+
+ if ($email === 'user2@foo.bar') {
+ $this->groupManager->expects($this->exactly(2))
+ ->method('getUserGroupIds')
+ ->willReturnMap([
+ [$user, ['group1', 'group2']],
+ [$user2, ['group1', 'group3']],
+ ]);
+ } else {
+ $this->groupManager->expects($this->exactly(2))
+ ->method('getUserGroupIds')
+ ->willReturnMap([
+ [$user, ['group1', 'group2']],
+ [$user3, ['group3', 'group3']],
+ ]);
+ }
+
+ $this->assertEquals($expects, $this->connector->findByUri($uri, 'principals/users'));
+ }
+
+ public static function findByUriWithGroupRestrictionDataProvider(): array {
+ return [
+ ['mailto:user2@foo.bar', 'user2@foo.bar', 'principals/users/user2'],
+ ['mailto:user3@foo.bar', 'user3@foo.bar', null],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('findByUriWithoutGroupRestrictionDataProvider')]
+ public function testFindByUriWithoutGroupRestriction(string $uri, string $email, string $expects): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareApiEnabled')
+ ->willReturn(true);
+
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(false);
+
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')->willReturn('user2');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')->willReturn('user3');
+
+ $this->userManager->expects($this->once())
+ ->method('getByEmail')
+ ->with($email)
+ ->willReturn([$email === 'user2@foo.bar' ? $user2 : $user3]);
+
+ $this->assertEquals($expects, $this->connector->findByUri($uri, 'principals/users'));
+ }
+
+ public static function findByUriWithoutGroupRestrictionDataProvider(): array {
+ return [
+ ['mailto:user2@foo.bar', 'user2@foo.bar', 'principals/users/user2'],
+ ['mailto:user3@foo.bar', 'user3@foo.bar', 'principals/users/user3'],
+ ];
+ }
+
+ public function testGetEmailAddressesOfPrincipal(): void {
+ $principal = [
+ '{http://sabredav.org/ns}email-address' => 'bar@company.org',
+ '{DAV:}alternate-URI-set' => [
+ '/some/url',
+ 'mailto:foo@bar.com',
+ 'mailto:duplicate@example.com',
+ ],
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => [
+ 'mailto:bernard@example.com',
+ 'mailto:bernard.desruisseaux@example.com',
+ ],
+ '{http://calendarserver.org/ns/}email-address-set' => [
+ 'mailto:duplicate@example.com',
+ 'mailto:user@some.org',
+ ],
+ ];
+
+ $expected = [
+ 'bar@company.org',
+ 'foo@bar.com',
+ 'duplicate@example.com',
+ 'bernard@example.com',
+ 'bernard.desruisseaux@example.com',
+ 'user@some.org',
+ ];
+ $actual = $this->connector->getEmailAddressesOfPrincipal($principal);
+ $this->assertEquals($expected, $actual);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php
new file mode 100644
index 00000000000..9d22befa201
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\PropFindMonitorPlugin;
+use OCA\DAV\Connector\Sabre\Server;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+use Test\TestCase;
+
+class PropFindMonitorPluginTest extends TestCase {
+
+ private PropFindMonitorPlugin $plugin;
+ private Server&MockObject $server;
+ private LoggerInterface&MockObject $logger;
+ private Request&MockObject $request;
+ private Response&MockObject $response;
+
+ public static function dataTest(): array {
+ $minQueriesTrigger = PropFindMonitorPlugin::THRESHOLD_QUERY_FACTOR
+ * PropFindMonitorPlugin::THRESHOLD_NODES;
+ return [
+ 'No queries logged' => [[], 0],
+ 'Plugins with queries in less than threshold nodes should not be logged' => [
+ [
+ 'propFind' => [
+ [
+ 'PluginName' => [
+ 'queries' => 100,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1]
+ ],
+ [],
+ ]
+ ],
+ 0
+ ],
+ 'Plugins with query-to-node ratio less than threshold should not be logged' => [
+ [
+ 'propFind' => [
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger - 1,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES ],
+ ],
+ [],
+ ]
+ ],
+ 0
+ ],
+ 'Plugins with more nodes scanned than queries executed should not be logged' => [
+ [
+ 'propFind' => [
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES * 2],
+ ],
+ [],]
+ ],
+ 0
+ ],
+ 'Plugins with queries only in highest depth level should not be logged' => [
+ [
+ 'propFind' => [
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1
+ ]
+ ],
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger * 2,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES
+ ]
+ ],
+ ]
+ ],
+ 0
+ ],
+ 'Plugins with too many queries should be logged' => [
+ [
+ 'propFind' => [
+ [
+ 'FirstPlugin' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES,
+ ],
+ 'SecondPlugin' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES,
+ ]
+ ],
+ [],
+ ]
+ ],
+ 2
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider dataTest
+ */
+ public function test(array $queries, $expectedLogCalls): void {
+ $this->plugin->initialize($this->server);
+ $this->server->expects($this->once())->method('getPluginQueries')
+ ->willReturn($queries);
+
+ $this->server->expects(empty($queries) ? $this->never() : $this->once())
+ ->method('getLogger')
+ ->willReturn($this->logger);
+
+ $this->logger->expects($this->exactly($expectedLogCalls))->method('error');
+ $this->plugin->afterResponse($this->request, $this->response);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->plugin = new PropFindMonitorPlugin();
+ $this->server = $this->createMock(Server::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->request = $this->createMock(Request::class);
+ $this->response = $this->createMock(Response::class);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php
new file mode 100644
index 00000000000..52fe3eba5bf
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\PropFindPreloadNotifyPlugin;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\ICollection;
+use Sabre\DAV\IFile;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
+use Test\TestCase;
+
+class PropFindPreloadNotifyPluginTest extends TestCase {
+
+ private Server&MockObject $server;
+ private PropFindPreloadNotifyPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->server = $this->createMock(Server::class);
+ $this->plugin = new PropFindPreloadNotifyPlugin();
+ }
+
+ public function testInitialize(): void {
+ $this->server
+ ->expects(self::once())
+ ->method('on')
+ ->with('propFind',
+ $this->anything(), 1);
+ $this->plugin->initialize($this->server);
+ }
+
+ public static function dataTestCollectionPreloadNotifier(): array {
+ return [
+ 'When node is not a collection, should not emit' => [
+ IFile::class,
+ 1,
+ false,
+ true
+ ],
+ 'When node is a collection but depth is zero, should not emit' => [
+ ICollection::class,
+ 0,
+ false,
+ true
+ ],
+ 'When node is a collection, and depth > 0, should emit' => [
+ ICollection::class,
+ 1,
+ true,
+ true
+ ],
+ 'When node is a collection, and depth is infinite, should emit'
+ => [
+ ICollection::class,
+ Server::DEPTH_INFINITY,
+ true,
+ true
+ ],
+ 'When called called handler returns false, it should be returned'
+ => [
+ ICollection::class,
+ 1,
+ true,
+ false
+ ]
+ ];
+ }
+
+ #[DataProvider(methodName: 'dataTestCollectionPreloadNotifier')]
+ public function testCollectionPreloadNotifier(string $nodeType, int $depth, bool $shouldEmit, bool $emitReturns):
+ void {
+ $this->plugin->initialize($this->server);
+ $propFind = $this->createMock(PropFind::class);
+ $propFind->expects(self::any())->method('getDepth')->willReturn($depth);
+ $node = $this->createMock($nodeType);
+
+ $expectation = $shouldEmit ? self::once() : self::never();
+ $this->server->expects($expectation)->method('emit')->with('preloadCollection',
+ [$propFind, $node])->willReturn($emitReturns);
+ $return = $this->plugin->collectionPreloadNotifier($propFind, $node);
+ $this->assertEquals($emitReturns, $return);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php
new file mode 100644
index 00000000000..e6f696ed160
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\PropfindCompressionPlugin;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+use Test\TestCase;
+
+class PropfindCompressionPluginTest extends TestCase {
+ private PropfindCompressionPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->plugin = new PropfindCompressionPlugin();
+ }
+
+ public function testNoHeader(): void {
+ $request = $this->createMock(Request::class);
+ $response = $this->createMock(Response::class);
+
+ $request->method('getHeader')
+ ->with('Accept-Encoding')
+ ->willReturn(null);
+
+ $response->expects($this->never())
+ ->method($this->anything());
+
+ $result = $this->plugin->compressResponse($request, $response);
+ $this->assertSame($response, $result);
+ }
+
+ public function testHeaderButNoGzip(): void {
+ $request = $this->createMock(Request::class);
+ $response = $this->createMock(Response::class);
+
+ $request->method('getHeader')
+ ->with('Accept-Encoding')
+ ->willReturn('deflate');
+
+ $response->expects($this->never())
+ ->method($this->anything());
+
+ $result = $this->plugin->compressResponse($request, $response);
+ $this->assertSame($response, $result);
+ }
+
+ public function testHeaderGzipButNoStringBody(): void {
+ $request = $this->createMock(Request::class);
+ $response = $this->createMock(Response::class);
+
+ $request->method('getHeader')
+ ->with('Accept-Encoding')
+ ->willReturn('deflate');
+
+ $response->method('getBody')
+ ->willReturn(5);
+
+ $result = $this->plugin->compressResponse($request, $response);
+ $this->assertSame($response, $result);
+ }
+
+
+ public function testProperGzip(): void {
+ $request = $this->createMock(Request::class);
+ $response = $this->createMock(Response::class);
+
+ $request->method('getHeader')
+ ->with('Accept-Encoding')
+ ->willReturn('gzip, deflate');
+
+ $response->method('getBody')
+ ->willReturn('my gzip test');
+
+ $response->expects($this->once())
+ ->method('setHeader')
+ ->with(
+ $this->equalTo('Content-Encoding'),
+ $this->equalTo('gzip')
+ );
+ $response->expects($this->once())
+ ->method('setBody')
+ ->with($this->callback(function ($data) {
+ $orig = gzdecode($data);
+ return $orig === 'my gzip test';
+ }));
+
+ $result = $this->plugin->compressResponse($request, $response);
+ $this->assertSame($response, $result);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php b/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php
new file mode 100644
index 00000000000..fef62b51c67
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php
@@ -0,0 +1,384 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector;
+
+use OCA\DAV\Connector\Sabre\PublicAuth;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Share\Exceptions\ShareNotFound;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Class PublicAuthTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\Connector
+ */
+class PublicAuthTest extends \Test\TestCase {
+
+ private ISession&MockObject $session;
+ private IRequest&MockObject $request;
+ private IManager&MockObject $shareManager;
+ private IThrottler&MockObject $throttler;
+ private LoggerInterface&MockObject $logger;
+ private IURLGenerator&MockObject $urlGenerator;
+ private PublicAuth $auth;
+
+ private bool|string $oldUser;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->session = $this->createMock(ISession::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->shareManager = $this->createMock(IManager::class);
+ $this->throttler = $this->createMock(IThrottler::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+
+ $this->auth = new PublicAuth(
+ $this->request,
+ $this->shareManager,
+ $this->session,
+ $this->throttler,
+ $this->logger,
+ $this->urlGenerator,
+ );
+
+ // Store current user
+ $this->oldUser = \OC_User::getUser();
+ }
+
+ protected function tearDown(): void {
+ \OC_User::setIncognitoMode(false);
+
+ // Set old user
+ \OC_User::setUserId($this->oldUser);
+ if ($this->oldUser !== false) {
+ \OC_Util::setupFS($this->oldUser);
+ }
+
+ parent::tearDown();
+ }
+
+ public function testGetToken(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $result = self::invokePrivate($this->auth, 'getToken');
+
+ $this->assertSame('GX9HSGQrGE', $result);
+ }
+
+ public function testGetTokenInvalid(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files');
+
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ self::invokePrivate($this->auth, 'getToken');
+ }
+
+ public function testCheckTokenValidShare(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn(null);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $result = self::invokePrivate($this->auth, 'checkToken');
+ $this->assertSame([true, 'principals/GX9HSGQrGE'], $result);
+ }
+
+ public function testCheckTokenInvalidShare(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $this->shareManager
+ ->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willThrowException(new ShareNotFound());
+
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ self::invokePrivate($this->auth, 'checkToken');
+ }
+
+ public function testCheckTokenAlreadyAuthenticated(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getShareType')->willReturn(42);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('42');
+
+ $result = self::invokePrivate($this->auth, 'checkToken');
+ $this->assertSame([true, 'principals/GX9HSGQrGE'], $result);
+ }
+
+ public function testCheckTokenPasswordNotAuthenticated(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(42);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(false);
+
+ $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
+ self::invokePrivate($this->auth, 'checkToken');
+ }
+
+ public function testCheckTokenPasswordAuthenticatedWrongShare(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(42);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(false);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('43');
+
+ $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
+ self::invokePrivate($this->auth, 'checkToken');
+ }
+
+ public function testNoShare(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willThrowException(new ShareNotFound());
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+
+ public function testShareNoPassword(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn(null);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordFancyShareType(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(42);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+
+
+ public function testSharePasswordRemote(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_REMOTE);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordLinkValidPassword(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_LINK);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(true);
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordMailValidPassword(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(true);
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testInvalidSharePasswordLinkValidSession(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_LINK);
+ $share->method('getId')->willReturn('42');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')
+ ->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(false);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('42');
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordLinkInvalidSession(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_LINK);
+ $share->method('getId')->willReturn('42');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')
+ ->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(false);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('43');
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+
+
+ public function testSharePasswordMailInvalidSession(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL);
+ $share->method('getId')->willReturn('42');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')
+ ->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(false);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('43');
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php
new file mode 100644
index 00000000000..6fe2d6ccabe
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php
@@ -0,0 +1,152 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2013-2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\Files\View;
+use OCA\DAV\Connector\Sabre\QuotaPlugin;
+use OCP\Files\FileInfo;
+use Test\TestCase;
+
+class QuotaPluginTest extends TestCase {
+ private \Sabre\DAV\Server $server;
+
+ private QuotaPlugin $plugin;
+
+ private function init(int $quota, string $checkedPath = ''): void {
+ $view = $this->buildFileViewMock((string)$quota, $checkedPath);
+ $this->server = new \Sabre\DAV\Server();
+ $this->plugin = new QuotaPlugin($view);
+ $this->plugin->initialize($this->server);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('lengthProvider')]
+ public function testLength(?int $expected, array $headers): void {
+ $this->init(0);
+
+ $this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers);
+ $length = $this->plugin->getLength();
+ $this->assertEquals($expected, $length);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('quotaOkayProvider')]
+ public function testCheckQuota(int $quota, array $headers): void {
+ $this->init($quota);
+
+ $this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers);
+ $result = $this->plugin->checkQuota('');
+ $this->assertTrue($result);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('quotaExceededProvider')]
+ public function testCheckExceededQuota(int $quota, array $headers): void {
+ $this->expectException(\Sabre\DAV\Exception\InsufficientStorage::class);
+
+ $this->init($quota);
+
+ $this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers);
+ $this->plugin->checkQuota('');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('quotaOkayProvider')]
+ public function testCheckQuotaOnPath(int $quota, array $headers): void {
+ $this->init($quota, 'sub/test.txt');
+
+ $this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers);
+ $result = $this->plugin->checkQuota('/sub/test.txt');
+ $this->assertTrue($result);
+ }
+
+ public static function quotaOkayProvider(): array {
+ return [
+ [1024, []],
+ [1024, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [1024, ['CONTENT-LENGTH' => '512']],
+ [1024, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
+
+ [FileInfo::SPACE_UNKNOWN, []],
+ [FileInfo::SPACE_UNKNOWN, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [FileInfo::SPACE_UNKNOWN, ['CONTENT-LENGTH' => '512']],
+ [FileInfo::SPACE_UNKNOWN, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
+
+ [FileInfo::SPACE_UNLIMITED, []],
+ [FileInfo::SPACE_UNLIMITED, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [FileInfo::SPACE_UNLIMITED, ['CONTENT-LENGTH' => '512']],
+ [FileInfo::SPACE_UNLIMITED, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
+ ];
+ }
+
+ public static function quotaExceededProvider(): array {
+ return [
+ [1023, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [511, ['CONTENT-LENGTH' => '512']],
+ [2047, ['OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => '1024']],
+ ];
+ }
+
+ public static function lengthProvider(): array {
+ return [
+ [null, []],
+ [1024, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [512, ['CONTENT-LENGTH' => '512']],
+ [2048, ['OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => '1024']],
+ [4096, ['OC-TOTAL-LENGTH' => '2048', 'X-EXPECTED-ENTITY-LENGTH' => '4096']],
+ [null, ['X-EXPECTED-ENTITY-LENGTH' => 'A']],
+ [null, ['CONTENT-LENGTH' => 'A']],
+ [1024, ['OC-TOTAL-LENGTH' => 'A', 'CONTENT-LENGTH' => '1024']],
+ [1024, ['OC-TOTAL-LENGTH' => 'A', 'X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [2048, ['OC-TOTAL-LENGTH' => '2048', 'X-EXPECTED-ENTITY-LENGTH' => 'A']],
+ [2048, ['OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => 'A']],
+ ];
+ }
+
+ public static function quotaChunkedOkProvider(): array {
+ return [
+ [1024, 0, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [1024, 0, ['CONTENT-LENGTH' => '512']],
+ [1024, 0, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
+ // with existing chunks (allowed size = total length - chunk total size)
+ [400, 128, ['X-EXPECTED-ENTITY-LENGTH' => '512']],
+ [400, 128, ['CONTENT-LENGTH' => '512']],
+ [400, 128, ['OC-TOTAL-LENGTH' => '512', 'CONTENT-LENGTH' => '500']],
+ // \OCP\Files\FileInfo::SPACE-UNKNOWN = -2
+ [-2, 0, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [-2, 0, ['CONTENT-LENGTH' => '512']],
+ [-2, 0, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
+ [-2, 128, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [-2, 128, ['CONTENT-LENGTH' => '512']],
+ [-2, 128, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
+ ];
+ }
+
+ public static function quotaChunkedFailProvider(): array {
+ return [
+ [400, 0, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [400, 0, ['CONTENT-LENGTH' => '512']],
+ [400, 0, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
+ // with existing chunks (allowed size = total length - chunk total size)
+ [380, 128, ['X-EXPECTED-ENTITY-LENGTH' => '512']],
+ [380, 128, ['CONTENT-LENGTH' => '512']],
+ [380, 128, ['OC-TOTAL-LENGTH' => '512', 'CONTENT-LENGTH' => '500']],
+ ];
+ }
+
+ private function buildFileViewMock(string $quota, string $checkedPath): View {
+ // mock filesystem
+ $view = $this->getMockBuilder(View::class)
+ ->onlyMethods(['free_space'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $view->expects($this->any())
+ ->method('free_space')
+ ->with($checkedPath)
+ ->willReturn($quota);
+
+ return $view;
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/auth.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Auth.php
index b728a8f3bd8..b01807d5bbb 100644
--- a/apps/dav/tests/unit/connector/sabre/requesttest/auth.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Auth.php
@@ -1,51 +1,30 @@
<?php
+
+declare(strict_types=1);
/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
+namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
-namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
-
+use OCP\IUserSession;
+use OCP\Server;
use Sabre\DAV\Auth\Backend\BackendInterface;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class Auth implements BackendInterface {
/**
- * @var string
- */
- private $user;
-
- /**
- * @var string
- */
- private $password;
-
- /**
* Auth constructor.
*
* @param string $user
* @param string $password
*/
- public function __construct($user, $password) {
- $this->user = $user;
- $this->password = $password;
+ public function __construct(
+ private $user,
+ private $password,
+ ) {
}
/**
@@ -76,8 +55,8 @@ class Auth implements BackendInterface {
* @param ResponseInterface $response
* @return array
*/
- function check(RequestInterface $request, ResponseInterface $response) {
- $userSession = \OC::$server->getUserSession();
+ public function check(RequestInterface $request, ResponseInterface $response) {
+ $userSession = Server::get(IUserSession::class);
$result = $userSession->login($this->user, $this->password);
if ($result) {
//we need to pass the user name, which may differ from login name
@@ -87,7 +66,7 @@ class Auth implements BackendInterface {
\OC::$server->getUserFolder($user);
return [true, "principals/$user"];
}
- return [false, "login failed"];
+ return [false, 'login failed'];
}
/**
@@ -111,7 +90,7 @@ class Auth implements BackendInterface {
* @param ResponseInterface $response
* @return void
*/
- function challenge(RequestInterface $request, ResponseInterface $response) {
+ public function challenge(RequestInterface $request, ResponseInterface $response): void {
// TODO: Implement challenge() method.
}
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php
new file mode 100644
index 00000000000..7d3488e6b5a
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
+
+use OCP\AppFramework\Http;
+use OCP\Files\FileInfo;
+
+/**
+ * Class DeleteTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest
+ */
+class DeleteTest extends RequestTestCase {
+ public function testBasicUpload(): void {
+ $user = self::getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'asd');
+ $mount = $view->getMount('foo.txt');
+ $internalPath = $view->getAbsolutePath();
+
+ // create a ghost file
+ $mount->getStorage()->unlink($mount->getInternalPath($internalPath));
+
+ // cache entry still exists
+ $this->assertInstanceOf(FileInfo::class, $view->getFileInfo('foo.txt'));
+
+ $response = $this->request($view, $user, 'pass', 'DELETE', '/foo.txt');
+
+ $this->assertEquals(Http::STATUS_NO_CONTENT, $response->getStatus());
+
+ // no longer in the cache
+ $this->assertFalse($view->getFileInfo('foo.txt'));
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php
new file mode 100644
index 00000000000..34171963ef0
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
+
+use OCP\AppFramework\Http;
+use OCP\Lock\ILockingProvider;
+
+/**
+ * Class DownloadTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest
+ */
+class DownloadTest extends RequestTestCase {
+ public function testDownload(): void {
+ $user = self::getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'bar');
+
+ $response = $this->request($view, $user, 'pass', 'GET', '/foo.txt');
+ $this->assertEquals(Http::STATUS_OK, $response->getStatus());
+ $this->assertEquals(stream_get_contents($response->getBody()), 'bar');
+ }
+
+ public function testDownloadWriteLocked(): void {
+ $user = self::getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'bar');
+
+ $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE);
+
+ $result = $this->request($view, $user, 'pass', 'GET', '/foo.txt', 'asd');
+ $this->assertEquals(Http::STATUS_LOCKED, $result->getStatus());
+ }
+
+ public function testDownloadReadLocked(): void {
+ $user = self::getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'bar');
+
+ $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED);
+
+ $response = $this->request($view, $user, 'pass', 'GET', '/foo.txt', 'asd');
+ $this->assertEquals(Http::STATUS_OK, $response->getStatus());
+ $this->assertEquals(stream_get_contents($response->getBody()), 'bar');
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php
new file mode 100644
index 00000000000..615490ddc92
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
+
+use OC\Files\View;
+use OCP\IConfig;
+use OCP\ITempManager;
+use OCP\Server;
+use Test\Traits\EncryptionTrait;
+
+/**
+ * Class EncryptionMasterKeyUploadTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest
+ */
+class EncryptionMasterKeyUploadTest extends UploadTest {
+ use EncryptionTrait;
+
+ protected function setupUser($name, $password): View {
+ $this->createUser($name, $password);
+ $tmpFolder = Server::get(ITempManager::class)->getTemporaryFolder();
+ $this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]);
+ // we use the master key
+ Server::get(IConfig::class)->setAppValue('encryption', 'useMasterKey', '1');
+ $this->setupForUser($name, $password);
+ $this->loginWithEncryption($name);
+ return new View('/' . $name . '/files');
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php
new file mode 100644
index 00000000000..efa7bb54cf8
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
+
+use OC\Files\View;
+use OCP\IConfig;
+use OCP\ITempManager;
+use OCP\Server;
+use Test\Traits\EncryptionTrait;
+
+/**
+ * Class EncryptionUploadTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest
+ */
+class EncryptionUploadTest extends UploadTest {
+ use EncryptionTrait;
+
+ protected function setupUser($name, $password): View {
+ $this->createUser($name, $password);
+ $tmpFolder = Server::get(ITempManager::class)->getTemporaryFolder();
+ $this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]);
+ // we use per-user keys
+ Server::get(IConfig::class)->setAppValue('encryption', 'useMasterKey', '0');
+ $this->setupForUser($name, $password);
+ $this->loginWithEncryption($name);
+ return new View('/' . $name . '/files');
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php
new file mode 100644
index 00000000000..0c53e4b1009
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
+
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
+
+class ExceptionPlugin extends ExceptionLoggerPlugin {
+ /**
+ * @var \Throwable[]
+ */
+ protected $exceptions = [];
+
+ public function logException(\Throwable $ex): void {
+ $exceptionClass = get_class($ex);
+ if (!isset($this->nonFatalExceptions[$exceptionClass])) {
+ $this->exceptions[] = $ex;
+ }
+ }
+
+ /**
+ * @return \Throwable[]
+ */
+ public function getExceptions() {
+ return $this->exceptions;
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php
new file mode 100644
index 00000000000..e6fa489fb24
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
+
+use OC\AllConfig;
+use OCP\IConfig;
+use OCP\Server;
+
+/**
+ * Class PartFileInRootUploadTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest
+ */
+class PartFileInRootUploadTest extends UploadTest {
+ protected function setUp(): void {
+ $config = Server::get(IConfig::class);
+ $mockConfig = $this->createMock(IConfig::class);
+ $mockConfig->expects($this->any())
+ ->method('getSystemValue')
+ ->willReturnCallback(function ($key, $default) use ($config) {
+ if ($key === 'part_file_in_storage') {
+ return false;
+ } else {
+ return $config->getSystemValue($key, $default);
+ }
+ });
+ $this->overwriteService(AllConfig::class, $mockConfig);
+ parent::setUp();
+ }
+
+ protected function tearDown(): void {
+ $this->restoreService('AllConfig');
+ parent::tearDown();
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php
index e3cdca5abfa..404dc7fa5d7 100644
--- a/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php
@@ -1,49 +1,37 @@
<?php
+
+declare(strict_types=1);
/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
+namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
-namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
-
+use OC\Files\View;
use OCA\DAV\Connector\Sabre\Server;
use OCA\DAV\Connector\Sabre\ServerFactory;
-use OC\Files\Mount\MountPoint;
-use OC\Files\Storage\StorageFactory;
-use OC\Files\Storage\Temporary;
-use OC\Files\View;
-use OCP\IUser;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Mount\IMountManager;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IPreview;
+use OCP\IRequest;
+use OCP\IRequestId;
+use OCP\ITagManager;
+use OCP\ITempManager;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use Psr\Log\LoggerInterface;
use Sabre\HTTP\Request;
use Test\TestCase;
use Test\Traits\MountProviderTrait;
use Test\Traits\UserTrait;
-abstract class RequestTest extends TestCase {
+abstract class RequestTestCase extends TestCase {
use UserTrait;
use MountProviderTrait;
-
- /**
- * @var \OCA\DAV\Connector\Sabre\ServerFactory
- */
- protected $serverFactory;
+ protected ServerFactory $serverFactory;
protected function getStream($string) {
$stream = fopen('php://temp', 'r+');
@@ -52,30 +40,33 @@ abstract class RequestTest extends TestCase {
return $stream;
}
- protected function setUp() {
+ protected function setUp(): void {
parent::setUp();
$this->serverFactory = new ServerFactory(
- \OC::$server->getConfig(),
- \OC::$server->getLogger(),
- \OC::$server->getDatabaseConnection(),
- \OC::$server->getUserSession(),
- \OC::$server->getMountManager(),
- \OC::$server->getTagManager(),
- $this->getMock('\OCP\IRequest')
+ \OCP\Server::get(IConfig::class),
+ \OCP\Server::get(LoggerInterface::class),
+ \OCP\Server::get(IDBConnection::class),
+ \OCP\Server::get(IUserSession::class),
+ \OCP\Server::get(IMountManager::class),
+ \OCP\Server::get(ITagManager::class),
+ $this->createMock(IRequest::class),
+ \OCP\Server::get(IPreview::class),
+ \OCP\Server::get(IEventDispatcher::class),
+ \OCP\Server::get(IFactory::class)->get('dav'),
);
}
- protected function setupUser($name, $password) {
+ protected function setupUser($name, $password): View {
$this->createUser($name, $password);
- $tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
+ $tmpFolder = \OCP\Server::get(ITempManager::class)->getTemporaryFolder();
$this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]);
- $this->loginAsUser($name);
+ self::loginAsUser($name);
return new View('/' . $name . '/files');
}
/**
- * @param \OC\Files\View $view the view to run the webdav server against
+ * @param View $view the view to run the webdav server against
* @param string $user
* @param string $password
* @param string $method
@@ -85,31 +76,36 @@ abstract class RequestTest extends TestCase {
* @return \Sabre\HTTP\Response
* @throws \Exception
*/
- protected function request($view, $user, $password, $method, $url, $body = null, $headers = null) {
+ protected function request($view, $user, $password, $method, $url, $body = null, $headers = []) {
if (is_string($body)) {
$body = $this->getStream($body);
}
$this->logout();
- $exceptionPlugin = new ExceptionPlugin('webdav', null);
+ $exceptionPlugin = new ExceptionPlugin('webdav', \OCP\Server::get(LoggerInterface::class));
$server = $this->getSabreServer($view, $user, $password, $exceptionPlugin);
$request = new Request($method, $url, $headers, $body);
// since sabre catches all exceptions we need to save them and throw them from outside the sabre server
- $originalServer = $_SERVER;
-
+ $serverParams = [];
if (is_array($headers)) {
foreach ($headers as $header => $value) {
- $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $header))] = $value;
+ $serverParams['HTTP_' . strtoupper(str_replace('-', '_', $header))] = $value;
}
}
+ $ncRequest = new \OC\AppFramework\Http\Request([
+ 'server' => $serverParams
+ ], $this->createMock(IRequestId::class), $this->createMock(IConfig::class), null);
+
+ $this->overwriteService(IRequest::class, $ncRequest);
$result = $this->makeRequest($server, $request);
+ $this->restoreService(IRequest::class);
+
foreach ($exceptionPlugin->getExceptions() as $exception) {
throw $exception;
}
- $_SERVER = $originalServer;
return $result;
}
@@ -135,8 +131,9 @@ abstract class RequestTest extends TestCase {
*/
protected function getSabreServer(View $view, $user, $password, ExceptionPlugin $exceptionPlugin) {
$authBackend = new Auth($user, $password);
+ $authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
- $server = $this->serverFactory->createServer('/', 'dummy', $authBackend, function () use ($view) {
+ $server = $this->serverFactory->createServer(false, '/', 'dummy', $authPlugin, function () use ($view) {
return $view;
});
$server->addPlugin($exceptionPlugin);
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php
new file mode 100644
index 00000000000..08d774e56b8
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
+
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+
+class Sapi {
+ /**
+ * @var \Sabre\HTTP\Response
+ */
+ private $response;
+
+ /**
+ * This static method will create a new Request object, based on the
+ * current PHP request.
+ *
+ * @return \Sabre\HTTP\Request
+ */
+ public function getRequest() {
+ return $this->request;
+ }
+
+ public function __construct(
+ private Request $request,
+ ) {
+ }
+
+ /**
+ * @param \Sabre\HTTP\Response $response
+ * @return void
+ */
+ public function sendResponse(Response $response): void {
+ // we need to copy the body since we close the source stream
+ $copyStream = fopen('php://temp', 'r+');
+ if (is_string($response->getBody())) {
+ fwrite($copyStream, $response->getBody());
+ } elseif (is_resource($response->getBody())) {
+ stream_copy_to_stream($response->getBody(), $copyStream);
+ }
+ rewind($copyStream);
+ $this->response = new Response($response->getStatus(), $response->getHeaders(), $copyStream);
+ }
+
+ /**
+ * @return \Sabre\HTTP\Response
+ */
+ public function getResponse() {
+ return $this->response;
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php
new file mode 100644
index 00000000000..5c6d0f03334
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
+
+use OCP\AppFramework\Http;
+use OCP\Lock\ILockingProvider;
+
+/**
+ * Class UploadTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest
+ */
+class UploadTest extends RequestTestCase {
+ public function testBasicUpload(): void {
+ $user = self::getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $this->assertFalse($view->file_exists('foo.txt'));
+ $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
+
+ $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
+ $this->assertTrue($view->file_exists('foo.txt'));
+ $this->assertEquals('asd', $view->file_get_contents('foo.txt'));
+
+ $info = $view->getFileInfo('foo.txt');
+ $this->assertInstanceOf('\OC\Files\FileInfo', $info);
+ $this->assertEquals(3, $info->getSize());
+ }
+
+ public function testUploadOverWrite(): void {
+ $user = self::getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'foobar');
+
+ $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
+
+ $this->assertEquals(Http::STATUS_NO_CONTENT, $response->getStatus());
+ $this->assertEquals('asd', $view->file_get_contents('foo.txt'));
+
+ $info = $view->getFileInfo('foo.txt');
+ $this->assertInstanceOf('\OC\Files\FileInfo', $info);
+ $this->assertEquals(3, $info->getSize());
+ }
+
+ public function testUploadOverWriteReadLocked(): void {
+ $user = self::getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'bar');
+
+ $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED);
+
+ $result = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
+ $this->assertEquals(Http::STATUS_LOCKED, $result->getStatus());
+ }
+
+ public function testUploadOverWriteWriteLocked(): void {
+ $user = self::getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+ $this->loginAsUser($user);
+
+ $view->file_put_contents('foo.txt', 'bar');
+
+ $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE);
+
+ $result = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
+ $this->assertEquals(Http::STATUS_LOCKED, $result->getStatus());
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php
new file mode 100644
index 00000000000..33f579eb913
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php
@@ -0,0 +1,282 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\File;
+use OCA\DAV\Connector\Sabre\Node;
+use OCA\DAV\Connector\Sabre\SharesPlugin;
+use OCA\DAV\Upload\UploadFile;
+use OCP\Files\Folder;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Tree;
+
+class SharesPluginTest extends \Test\TestCase {
+ public const SHARETYPES_PROPERTYNAME = SharesPlugin::SHARETYPES_PROPERTYNAME;
+
+ private \Sabre\DAV\Server $server;
+ private \Sabre\DAV\Tree&MockObject $tree;
+ private \OCP\Share\IManager&MockObject $shareManager;
+ private Folder&MockObject $userFolder;
+ private SharesPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->server = new \Sabre\DAV\Server();
+ $this->tree = $this->createMock(Tree::class);
+ $this->shareManager = $this->createMock(IManager::class);
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())
+ ->method('getUID')
+ ->willReturn('user1');
+ $userSession = $this->createMock(IUserSession::class);
+ $userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->userFolder = $this->createMock(Folder::class);
+
+ $this->plugin = new SharesPlugin(
+ $this->tree,
+ $userSession,
+ $this->userFolder,
+ $this->shareManager
+ );
+ $this->plugin->initialize($this->server);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('sharesGetPropertiesDataProvider')]
+ public function testGetProperties(array $shareTypes): void {
+ $sabreNode = $this->createMock(Node::class);
+ $sabreNode->expects($this->any())
+ ->method('getId')
+ ->willReturn(123);
+ $sabreNode->expects($this->any())
+ ->method('getPath')
+ ->willReturn('/subdir');
+
+ // node API nodes
+ $node = $this->createMock(Folder::class);
+
+ $sabreNode->method('getNode')
+ ->willReturn($node);
+
+ $this->shareManager->expects($this->any())
+ ->method('getSharesBy')
+ ->with(
+ $this->equalTo('user1'),
+ $this->anything(),
+ $this->equalTo($node),
+ $this->equalTo(false),
+ $this->equalTo(-1)
+ )
+ ->willReturnCallback(function ($userId, $requestedShareType, $node, $flag, $limit) use ($shareTypes) {
+ if (in_array($requestedShareType, $shareTypes)) {
+ $share = $this->createMock(IShare::class);
+ $share->method('getShareType')
+ ->willReturn($requestedShareType);
+ return [$share];
+ }
+ return [];
+ });
+
+ $this->shareManager->expects($this->any())
+ ->method('getSharedWith')
+ ->with(
+ $this->equalTo('user1'),
+ $this->anything(),
+ $this->equalTo($node),
+ $this->equalTo(-1)
+ )
+ ->willReturn([]);
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummyPath',
+ [self::SHARETYPES_PROPERTYNAME],
+ 0
+ );
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $sabreNode
+ );
+
+ $result = $propFind->getResultForMultiStatus();
+
+ $this->assertEmpty($result[404]);
+ unset($result[404]);
+ $this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('sharesGetPropertiesDataProvider')]
+ public function testPreloadThenGetProperties(array $shareTypes): void {
+ $sabreNode1 = $this->createMock(File::class);
+ $sabreNode1->method('getId')
+ ->willReturn(111);
+ $sabreNode2 = $this->createMock(File::class);
+ $sabreNode2->method('getId')
+ ->willReturn(222);
+ $sabreNode2->method('getPath')
+ ->willReturn('/subdir/foo');
+
+ $sabreNode = $this->createMock(Directory::class);
+ $sabreNode->method('getId')
+ ->willReturn(123);
+ // never, because we use getDirectoryListing from the Node API instead
+ $sabreNode->expects($this->never())
+ ->method('getChildren');
+ $sabreNode->expects($this->any())
+ ->method('getPath')
+ ->willReturn('/subdir');
+
+ // node API nodes
+ $node = $this->createMock(Folder::class);
+ $node->method('getId')
+ ->willReturn(123);
+ $node1 = $this->createMock(\OC\Files\Node\File::class);
+ $node1->method('getId')
+ ->willReturn(111);
+ $node2 = $this->createMock(\OC\Files\Node\File::class);
+ $node2->method('getId')
+ ->willReturn(222);
+
+ $sabreNode->method('getNode')
+ ->willReturn($node);
+ $sabreNode1->method('getNode')
+ ->willReturn($node1);
+ $sabreNode2->method('getNode')
+ ->willReturn($node2);
+
+ $dummyShares = array_map(function ($type) {
+ $share = $this->createMock(IShare::class);
+ $share->expects($this->any())
+ ->method('getShareType')
+ ->willReturn($type);
+ return $share;
+ }, $shareTypes);
+
+ $this->shareManager->expects($this->any())
+ ->method('getSharesBy')
+ ->with(
+ $this->equalTo('user1'),
+ $this->anything(),
+ $this->anything(),
+ $this->equalTo(false),
+ $this->equalTo(-1)
+ )
+ ->willReturnCallback(function ($userId, $requestedShareType, $node, $flag, $limit) use ($shareTypes, $dummyShares) {
+ if ($node->getId() === 111 && in_array($requestedShareType, $shareTypes)) {
+ foreach ($dummyShares as $dummyShare) {
+ if ($dummyShare->getShareType() === $requestedShareType) {
+ return [$dummyShare];
+ }
+ }
+ }
+
+ return [];
+ });
+
+ $this->shareManager->expects($this->any())
+ ->method('getSharedWith')
+ ->with(
+ $this->equalTo('user1'),
+ $this->anything(),
+ $this->equalTo($node),
+ $this->equalTo(-1)
+ )
+ ->willReturn([]);
+
+ $this->shareManager->expects($this->any())
+ ->method('getSharesInFolder')
+ ->with(
+ $this->equalTo('user1'),
+ $this->anything(),
+ $this->equalTo(true)
+ )
+ ->willReturnCallback(function ($userId, $node, $flag) use ($shareTypes, $dummyShares) {
+ return [111 => $dummyShares];
+ });
+
+ // simulate sabre recursive PROPFIND traversal
+ $propFindRoot = new \Sabre\DAV\PropFind(
+ '/subdir',
+ [self::SHARETYPES_PROPERTYNAME],
+ 1
+ );
+ $propFind1 = new \Sabre\DAV\PropFind(
+ '/subdir/test.txt',
+ [self::SHARETYPES_PROPERTYNAME],
+ 0
+ );
+ $propFind2 = new \Sabre\DAV\PropFind(
+ '/subdir/test2.txt',
+ [self::SHARETYPES_PROPERTYNAME],
+ 0
+ );
+
+ $this->server->emit('preloadCollection', [$propFindRoot, $sabreNode]);
+ $this->plugin->handleGetProperties(
+ $propFindRoot,
+ $sabreNode
+ );
+ $this->plugin->handleGetProperties(
+ $propFind1,
+ $sabreNode1
+ );
+ $this->plugin->handleGetProperties(
+ $propFind2,
+ $sabreNode2
+ );
+
+ $result = $propFind1->getResultForMultiStatus();
+
+ $this->assertEmpty($result[404]);
+ unset($result[404]);
+ $this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes());
+ }
+
+ public static function sharesGetPropertiesDataProvider(): array {
+ return [
+ [[]],
+ [[IShare::TYPE_USER]],
+ [[IShare::TYPE_GROUP]],
+ [[IShare::TYPE_LINK]],
+ [[IShare::TYPE_REMOTE]],
+ [[IShare::TYPE_ROOM]],
+ [[IShare::TYPE_DECK]],
+ [[IShare::TYPE_SCIENCEMESH]],
+ [[IShare::TYPE_USER, IShare::TYPE_GROUP]],
+ [[IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK]],
+ [[IShare::TYPE_USER, IShare::TYPE_LINK]],
+ [[IShare::TYPE_GROUP, IShare::TYPE_LINK]],
+ [[IShare::TYPE_USER, IShare::TYPE_REMOTE]],
+ ];
+ }
+
+ public function testGetPropertiesSkipChunks(): void {
+ $sabreNode = $this->createMock(UploadFile::class);
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummyPath',
+ [self::SHARETYPES_PROPERTYNAME],
+ 0
+ );
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $sabreNode
+ );
+
+ $result = $propFind->getResultForMultiStatus();
+ $this->assertCount(1, $result[404]);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php
new file mode 100644
index 00000000000..554a4a1424e
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php
@@ -0,0 +1,410 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2014-2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\File;
+use OCA\DAV\Connector\Sabre\Node;
+use OCA\DAV\Connector\Sabre\TagList;
+use OCA\DAV\Connector\Sabre\TagsPlugin;
+use OCA\DAV\Upload\UploadFile;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\ITagManager;
+use OCP\ITags;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Tree;
+
+class TagsPluginTest extends \Test\TestCase {
+ public const TAGS_PROPERTYNAME = TagsPlugin::TAGS_PROPERTYNAME;
+ public const FAVORITE_PROPERTYNAME = TagsPlugin::FAVORITE_PROPERTYNAME;
+ public const TAG_FAVORITE = TagsPlugin::TAG_FAVORITE;
+
+ private \Sabre\DAV\Server $server;
+ private Tree&MockObject $tree;
+ private ITagManager&MockObject $tagManager;
+ private ITags&MockObject $tagger;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private IUserSession&MockObject $userSession;
+ private TagsPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->server = new \Sabre\DAV\Server();
+ $this->tree = $this->createMock(Tree::class);
+ $this->tagger = $this->createMock(ITags::class);
+ $this->tagManager = $this->createMock(ITagManager::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $user = $this->createMock(IUser::class);
+
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->userSession->expects($this->any())
+ ->method('getUser')
+ ->withAnyParameters()
+ ->willReturn($user);
+ $this->tagManager->expects($this->any())
+ ->method('load')
+ ->with('files')
+ ->willReturn($this->tagger);
+ $this->plugin = new TagsPlugin($this->tree, $this->tagManager, $this->eventDispatcher, $this->userSession);
+ $this->plugin->initialize($this->server);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagsGetPropertiesDataProvider')]
+ public function testGetProperties(array $tags, array $requestedProperties, array $expectedProperties): void {
+ $node = $this->createMock(Node::class);
+ $node->expects($this->any())
+ ->method('getId')
+ ->willReturn(123);
+
+ $expectedCallCount = 0;
+ if (count($requestedProperties) > 0) {
+ $expectedCallCount = 1;
+ }
+
+ $this->tagger->expects($this->exactly($expectedCallCount))
+ ->method('getTagsForObjects')
+ ->with($this->equalTo([123]))
+ ->willReturn([123 => $tags]);
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummyPath',
+ $requestedProperties,
+ 0
+ );
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $result = $propFind->getResultForMultiStatus();
+
+ $this->assertEmpty($result[404]);
+ unset($result[404]);
+ $this->assertEquals($expectedProperties, $result);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagsGetPropertiesDataProvider')]
+ public function testPreloadThenGetProperties(array $tags, array $requestedProperties, array $expectedProperties): void {
+ $node1 = $this->createMock(File::class);
+ $node1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $node2 = $this->createMock(File::class);
+ $node2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+
+ $expectedCallCount = 0;
+ if (count($requestedProperties) > 0) {
+ // this guarantees that getTagsForObjects
+ // is only called once and then the tags
+ // are cached
+ $expectedCallCount = 1;
+ }
+
+ $node = $this->createMock(Directory::class);
+ $node->expects($this->any())
+ ->method('getId')
+ ->willReturn(123);
+ $node->expects($this->exactly($expectedCallCount))
+ ->method('getChildren')
+ ->willReturn([$node1, $node2]);
+
+ $this->tagger->expects($this->exactly($expectedCallCount))
+ ->method('getTagsForObjects')
+ ->with($this->equalTo([123, 111, 222]))
+ ->willReturn(
+ [
+ 111 => $tags,
+ 123 => $tags
+ ]
+ );
+
+ // simulate sabre recursive PROPFIND traversal
+ $propFindRoot = new \Sabre\DAV\PropFind(
+ '/subdir',
+ $requestedProperties,
+ 1
+ );
+ $propFind1 = new \Sabre\DAV\PropFind(
+ '/subdir/test.txt',
+ $requestedProperties,
+ 0
+ );
+ $propFind2 = new \Sabre\DAV\PropFind(
+ '/subdir/test2.txt',
+ $requestedProperties,
+ 0
+ );
+
+ $this->server->emit('preloadCollection', [$propFindRoot, $node]);
+
+ $this->plugin->handleGetProperties(
+ $propFindRoot,
+ $node
+ );
+ $this->plugin->handleGetProperties(
+ $propFind1,
+ $node1
+ );
+ $this->plugin->handleGetProperties(
+ $propFind2,
+ $node2
+ );
+
+ $result = $propFind1->getResultForMultiStatus();
+
+ $this->assertEmpty($result[404]);
+ unset($result[404]);
+ $this->assertEquals($expectedProperties, $result);
+ }
+
+ public static function tagsGetPropertiesDataProvider(): array {
+ return [
+ // request both, receive both
+ [
+ ['tag1', 'tag2', self::TAG_FAVORITE],
+ [self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME],
+ [
+ 200 => [
+ self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2']),
+ self::FAVORITE_PROPERTYNAME => true,
+ ]
+ ]
+ ],
+ // request tags alone
+ [
+ ['tag1', 'tag2', self::TAG_FAVORITE],
+ [self::TAGS_PROPERTYNAME],
+ [
+ 200 => [
+ self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2']),
+ ]
+ ]
+ ],
+ // request fav alone
+ [
+ ['tag1', 'tag2', self::TAG_FAVORITE],
+ [self::FAVORITE_PROPERTYNAME],
+ [
+ 200 => [
+ self::FAVORITE_PROPERTYNAME => true,
+ ]
+ ]
+ ],
+ // request none
+ [
+ ['tag1', 'tag2', self::TAG_FAVORITE],
+ [],
+ [
+ 200 => []
+ ],
+ ],
+ // request both with none set, receive both
+ [
+ [],
+ [self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME],
+ [
+ 200 => [
+ self::TAGS_PROPERTYNAME => new TagList([]),
+ self::FAVORITE_PROPERTYNAME => false,
+ ]
+ ]
+ ],
+ ];
+ }
+
+ public function testGetPropertiesSkipChunks(): void {
+ $sabreNode = $this->createMock(UploadFile::class);
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummyPath',
+ [self::TAGS_PROPERTYNAME, self::TAG_FAVORITE],
+ 0
+ );
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $sabreNode
+ );
+
+ $result = $propFind->getResultForMultiStatus();
+ $this->assertCount(2, $result[404]);
+ }
+
+ public function testUpdateTags(): void {
+ // this test will replace the existing tags "tagremove" with "tag1" and "tag2"
+ // and keep "tagkeep"
+ $node = $this->createMock(Node::class);
+ $node->expects($this->any())
+ ->method('getId')
+ ->willReturn(123);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->willReturn($node);
+
+ $this->tagger->expects($this->once())
+ ->method('getTagsForObjects')
+ ->with($this->equalTo([123]))
+ ->willReturn([123 => ['tagkeep', 'tagremove', self::TAG_FAVORITE]]);
+
+ // then tag as tag1 and tag2
+ $calls = [
+ [123, 'tag1'],
+ [123, 'tag2'],
+ ];
+ $this->tagger->expects($this->exactly(count($calls)))
+ ->method('tagAs')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ // it will untag tag3
+ $this->tagger->expects($this->once())
+ ->method('unTag')
+ ->with(123, 'tagremove');
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch([
+ self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2', 'tagkeep'])
+ ]);
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ // all requested properties removed, as they were processed already
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]);
+ $this->assertArrayNotHasKey(self::FAVORITE_PROPERTYNAME, $result);
+ }
+
+ public function testUpdateTagsFromScratch(): void {
+ $node = $this->createMock(Node::class);
+ $node->expects($this->any())
+ ->method('getId')
+ ->willReturn(123);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->willReturn($node);
+
+ $this->tagger->expects($this->once())
+ ->method('getTagsForObjects')
+ ->with($this->equalTo([123]))
+ ->willReturn([]);
+
+ // then tag as tag1 and tag2
+ $calls = [
+ [123, 'tag1'],
+ [123, 'tag2'],
+ ];
+ $this->tagger->expects($this->exactly(count($calls)))
+ ->method('tagAs')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch([
+ self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2'])
+ ]);
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ // all requested properties removed, as they were processed already
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]);
+ $this->assertArrayNotHasKey(self::FAVORITE_PROPERTYNAME, $result);
+ }
+
+ public function testUpdateFav(): void {
+ // this test will replace the existing tags "tagremove" with "tag1" and "tag2"
+ // and keep "tagkeep"
+ $node = $this->createMock(Node::class);
+ $node->expects($this->any())
+ ->method('getId')
+ ->willReturn(123);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->willReturn($node);
+
+ // set favorite tag
+ $this->tagger->expects($this->once())
+ ->method('tagAs')
+ ->with(123, self::TAG_FAVORITE);
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch([
+ self::FAVORITE_PROPERTYNAME => true
+ ]);
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ // all requested properties removed, as they were processed already
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertArrayNotHasKey(self::TAGS_PROPERTYNAME, $result);
+ $this->assertEquals(200, $result[self::FAVORITE_PROPERTYNAME]);
+
+ // unfavorite now
+ // set favorite tag
+ $this->tagger->expects($this->once())
+ ->method('unTag')
+ ->with(123, self::TAG_FAVORITE);
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch([
+ self::FAVORITE_PROPERTYNAME => false
+ ]);
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ // all requested properties removed, as they were processed already
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertArrayNotHasKey(self::TAGS_PROPERTYNAME, $result);
+ $this->assertEquals(200, $result[self::FAVORITE_PROPERTYNAME]);
+ }
+}
diff --git a/apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php b/apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php
new file mode 100644
index 00000000000..9aa0ef3a2a7
--- /dev/null
+++ b/apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV\Controller;
+
+use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Controller\BirthdayCalendarController;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class BirthdayCalendarControllerTest extends TestCase {
+ private IConfig&MockObject $config;
+ private IRequest&MockObject $request;
+ private IDBConnection&MockObject $db;
+ private IJobList&MockObject $jobList;
+ private IUserManager&MockObject $userManager;
+ private CalDavBackend&MockObject $caldav;
+ private BirthdayCalendarController $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->db = $this->createMock(IDBConnection::class);
+ $this->jobList = $this->createMock(IJobList::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->caldav = $this->createMock(CalDavBackend::class);
+
+ $this->controller = new BirthdayCalendarController('dav',
+ $this->request, $this->db, $this->config, $this->jobList,
+ $this->userManager, $this->caldav);
+ }
+
+ public function testEnable(): void {
+ $this->config->expects($this->once())
+ ->method('setAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'yes');
+
+ $this->userManager->expects($this->once())
+ ->method('callForSeenUsers')
+ ->willReturnCallback(function ($closure): void {
+ $user1 = $this->createMock(IUser::class);
+ $user1->method('getUID')->willReturn('uid1');
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getUID')->willReturn('uid2');
+ $user3 = $this->createMock(IUser::class);
+ $user3->method('getUID')->willReturn('uid3');
+
+ $closure($user1);
+ $closure($user2);
+ $closure($user3);
+ });
+
+ $calls = [
+ [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid1']],
+ [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid2']],
+ [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid3']],
+ ];
+ $this->jobList->expects($this->exactly(3))
+ ->method('add')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ $response = $this->controller->enable();
+ $this->assertInstanceOf(JSONResponse::class, $response);
+ }
+
+ public function testDisable(): void {
+ $this->config->expects($this->once())
+ ->method('setAppValue')
+ ->with('dav', 'generateBirthdayCalendar', 'no');
+ $this->jobList->expects($this->once())
+ ->method('remove')
+ ->with(GenerateBirthdayCalendarBackgroundJob::class);
+ $this->caldav->expects($this->once())
+ ->method('deleteAllBirthdayCalendars');
+
+ $response = $this->controller->disable();
+ $this->assertInstanceOf(JSONResponse::class, $response);
+ }
+}
diff --git a/apps/dav/tests/unit/Controller/DirectControllerTest.php b/apps/dav/tests/unit/Controller/DirectControllerTest.php
new file mode 100644
index 00000000000..837adde1da7
--- /dev/null
+++ b/apps/dav/tests/unit/Controller/DirectControllerTest.php
@@ -0,0 +1,138 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV\Controller;
+
+use OCA\DAV\Controller\DirectController;
+use OCA\DAV\Db\Direct;
+use OCA\DAV\Db\DirectMapper;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCS\OCSBadRequestException;
+use OCP\AppFramework\OCS\OCSNotFoundException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\IRequest;
+use OCP\IURLGenerator;
+use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class DirectControllerTest extends TestCase {
+ private IRootFolder&MockObject $rootFolder;
+ private DirectMapper&MockObject $directMapper;
+ private ISecureRandom&MockObject $random;
+ private ITimeFactory&MockObject $timeFactory;
+ private IURLGenerator&MockObject $urlGenerator;
+ private IEventDispatcher&MockObject $eventDispatcher;
+
+ private DirectController $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->rootFolder = $this->createMock(IRootFolder::class);
+ $this->directMapper = $this->createMock(DirectMapper::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+
+ $this->controller = new DirectController(
+ 'dav',
+ $this->createMock(IRequest::class),
+ $this->rootFolder,
+ 'awesomeUser',
+ $this->directMapper,
+ $this->random,
+ $this->timeFactory,
+ $this->urlGenerator,
+ $this->eventDispatcher
+ );
+ }
+
+ public function testGetUrlNonExistingFileId(): void {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with('awesomeUser')
+ ->willReturn($userFolder);
+
+ $userFolder->method('getById')
+ ->with(101)
+ ->willReturn([]);
+
+ $this->expectException(OCSNotFoundException::class);
+ $this->controller->getUrl(101);
+ }
+
+ public function testGetUrlForFolder(): void {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with('awesomeUser')
+ ->willReturn($userFolder);
+
+ $folder = $this->createMock(Folder::class);
+
+ $userFolder->method('getFirstNodeById')
+ ->with(101)
+ ->willReturn($folder);
+
+ $this->expectException(OCSBadRequestException::class);
+ $this->controller->getUrl(101);
+ }
+
+ public function testGetUrlValid(): void {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with('awesomeUser')
+ ->willReturn($userFolder);
+
+ $file = $this->createMock(File::class);
+
+ $this->timeFactory->method('getTime')
+ ->willReturn(42);
+
+ $userFolder->method('getFirstNodeById')
+ ->with(101)
+ ->willReturn($file);
+
+ $userFolder->method('getRelativePath')
+ ->willReturn('/path');
+
+ $this->random->method('generate')
+ ->with(
+ 60,
+ ISecureRandom::CHAR_ALPHANUMERIC
+ )->willReturn('superduperlongtoken');
+
+ $this->directMapper->expects($this->once())
+ ->method('insert')
+ ->willReturnCallback(function (Direct $direct) {
+ $this->assertSame('awesomeUser', $direct->getUserId());
+ $this->assertSame(101, $direct->getFileId());
+ $this->assertSame('superduperlongtoken', $direct->getToken());
+ $this->assertSame(42 + 60 * 60 * 8, $direct->getExpiration());
+
+ return $direct;
+ });
+
+ $this->urlGenerator->method('getAbsoluteURL')
+ ->willReturnCallback(function (string $url) {
+ return 'https://my.nextcloud/' . $url;
+ });
+
+ $result = $this->controller->getUrl(101);
+
+ $this->assertInstanceOf(DataResponse::class, $result);
+ $this->assertSame([
+ 'url' => 'https://my.nextcloud/remote.php/direct/superduperlongtoken',
+ ], $result->getData());
+ }
+}
diff --git a/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php b/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php
new file mode 100644
index 00000000000..15b18d6c1b1
--- /dev/null
+++ b/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php
@@ -0,0 +1,461 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\DAV\Controller;
+
+use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
+use OCA\DAV\Controller\InvitationResponseController;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IExpressionBuilder;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\ITip\Message;
+use Test\TestCase;
+
+class InvitationResponseControllerTest extends TestCase {
+ private IDBConnection&MockObject $dbConnection;
+ private IRequest&MockObject $request;
+ private ITimeFactory&MockObject $timeFactory;
+ private InvitationResponseServer&MockObject $responseServer;
+ private InvitationResponseController $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->dbConnection = $this->createMock(IDBConnection::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->responseServer = $this->createMock(InvitationResponseServer::class);
+
+ $this->controller = new InvitationResponseController(
+ 'appName',
+ $this->request,
+ $this->dbConnection,
+ $this->timeFactory,
+ $this->responseServer
+ );
+ }
+
+ public static function attendeeProvider(): array {
+ return [
+ 'local attendee' => [false],
+ 'external attendee' => [true]
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')]
+ public function testAccept(bool $isExternalAttendee): void {
+ $this->buildQueryExpects('TOKEN123', [
+ 'id' => 0,
+ 'uid' => 'this-is-the-events-uid',
+ 'recurrenceid' => null,
+ 'attendee' => 'mailto:attendee@foo.bar',
+ 'organizer' => 'mailto:organizer@foo.bar',
+ 'sequence' => null,
+ 'token' => 'TOKEN123',
+ 'expiration' => 420000,
+ ], 1337);
+
+ $expected = <<<EOF
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:attendee@foo.bar
+ORGANIZER:mailto:organizer@foo.bar
+UID:this-is-the-events-uid
+SEQUENCE:0
+REQUEST-STATUS:2.0;Success
+DTSTAMP:19700101T002217Z
+END:VEVENT
+END:VCALENDAR
+
+EOF;
+ $expected = preg_replace('~\R~u', "\r\n", $expected);
+
+ $called = false;
+ $this->responseServer->expects($this->once())
+ ->method('handleITipMessage')
+ ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void {
+ $called = true;
+ $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
+ $this->assertEquals('VEVENT', $iTipMessage->component);
+ $this->assertEquals('REPLY', $iTipMessage->method);
+ $this->assertEquals(null, $iTipMessage->sequence);
+ $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender);
+ if ($isExternalAttendee) {
+ $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient);
+ } else {
+ $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient);
+ }
+
+ $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
+
+ $this->assertEquals($expected, $iTipMessage->message->serialize());
+ });
+ $this->responseServer->expects($this->once())
+ ->method('isExternalAttendee')
+ ->willReturn($isExternalAttendee);
+
+ $response = $this->controller->accept('TOKEN123');
+ $this->assertInstanceOf(TemplateResponse::class, $response);
+ $this->assertEquals('schedule-response-success', $response->getTemplateName());
+ $this->assertEquals([], $response->getParams());
+ $this->assertTrue($called);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')]
+ public function testAcceptSequence(bool $isExternalAttendee): void {
+ $this->buildQueryExpects('TOKEN123', [
+ 'id' => 0,
+ 'uid' => 'this-is-the-events-uid',
+ 'recurrenceid' => null,
+ 'attendee' => 'mailto:attendee@foo.bar',
+ 'organizer' => 'mailto:organizer@foo.bar',
+ 'sequence' => 1337,
+ 'token' => 'TOKEN123',
+ 'expiration' => 420000,
+ ], 1337);
+
+ $expected = <<<EOF
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:attendee@foo.bar
+ORGANIZER:mailto:organizer@foo.bar
+UID:this-is-the-events-uid
+SEQUENCE:1337
+REQUEST-STATUS:2.0;Success
+DTSTAMP:19700101T002217Z
+END:VEVENT
+END:VCALENDAR
+
+EOF;
+ $expected = preg_replace('~\R~u', "\r\n", $expected);
+
+ $called = false;
+ $this->responseServer->expects($this->once())
+ ->method('handleITipMessage')
+ ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void {
+ $called = true;
+ $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
+ $this->assertEquals('VEVENT', $iTipMessage->component);
+ $this->assertEquals('REPLY', $iTipMessage->method);
+ $this->assertEquals(1337, $iTipMessage->sequence);
+ $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender);
+ if ($isExternalAttendee) {
+ $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient);
+ } else {
+ $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient);
+ }
+
+ $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
+
+ $this->assertEquals($expected, $iTipMessage->message->serialize());
+ });
+ $this->responseServer->expects($this->once())
+ ->method('isExternalAttendee')
+ ->willReturn($isExternalAttendee);
+
+ $response = $this->controller->accept('TOKEN123');
+ $this->assertInstanceOf(TemplateResponse::class, $response);
+ $this->assertEquals('schedule-response-success', $response->getTemplateName());
+ $this->assertEquals([], $response->getParams());
+ $this->assertTrue($called);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')]
+ public function testAcceptRecurrenceId(bool $isExternalAttendee): void {
+ $this->buildQueryExpects('TOKEN123', [
+ 'id' => 0,
+ 'uid' => 'this-is-the-events-uid',
+ 'recurrenceid' => "RECURRENCE-ID;TZID=Europe/Berlin:20180726T150000\n",
+ 'attendee' => 'mailto:attendee@foo.bar',
+ 'organizer' => 'mailto:organizer@foo.bar',
+ 'sequence' => null,
+ 'token' => 'TOKEN123',
+ 'expiration' => 420000,
+ ], 1337);
+
+ $expected = <<<EOF
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:attendee@foo.bar
+ORGANIZER:mailto:organizer@foo.bar
+UID:this-is-the-events-uid
+SEQUENCE:0
+REQUEST-STATUS:2.0;Success
+RECURRENCE-ID;TZID=Europe/Berlin:20180726T150000
+DTSTAMP:19700101T002217Z
+END:VEVENT
+END:VCALENDAR
+
+EOF;
+ $expected = preg_replace('~\R~u', "\r\n", $expected);
+
+ $called = false;
+ $this->responseServer->expects($this->once())
+ ->method('handleITipMessage')
+ ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void {
+ $called = true;
+ $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
+ $this->assertEquals('VEVENT', $iTipMessage->component);
+ $this->assertEquals('REPLY', $iTipMessage->method);
+ $this->assertEquals(0, $iTipMessage->sequence);
+ $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender);
+ if ($isExternalAttendee) {
+ $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient);
+ } else {
+ $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient);
+ }
+
+ $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
+
+ $this->assertEquals($expected, $iTipMessage->message->serialize());
+ });
+ $this->responseServer->expects($this->once())
+ ->method('isExternalAttendee')
+ ->willReturn($isExternalAttendee);
+
+ $response = $this->controller->accept('TOKEN123');
+ $this->assertInstanceOf(TemplateResponse::class, $response);
+ $this->assertEquals('schedule-response-success', $response->getTemplateName());
+ $this->assertEquals([], $response->getParams());
+ $this->assertTrue($called);
+ }
+
+ public function testAcceptTokenNotFound(): void {
+ $this->buildQueryExpects('TOKEN123', null, 1337);
+
+ $response = $this->controller->accept('TOKEN123');
+ $this->assertInstanceOf(TemplateResponse::class, $response);
+ $this->assertEquals('schedule-response-error', $response->getTemplateName());
+ $this->assertEquals([], $response->getParams());
+ }
+
+ public function testAcceptExpiredToken(): void {
+ $this->buildQueryExpects('TOKEN123', [
+ 'id' => 0,
+ 'uid' => 'this-is-the-events-uid',
+ 'recurrenceid' => null,
+ 'attendee' => 'mailto:attendee@foo.bar',
+ 'organizer' => 'mailto:organizer@foo.bar',
+ 'sequence' => null,
+ 'token' => 'TOKEN123',
+ 'expiration' => 42,
+ ], 1337);
+
+ $response = $this->controller->accept('TOKEN123');
+ $this->assertInstanceOf(TemplateResponse::class, $response);
+ $this->assertEquals('schedule-response-error', $response->getTemplateName());
+ $this->assertEquals([], $response->getParams());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')]
+ public function testDecline(bool $isExternalAttendee): void {
+ $this->buildQueryExpects('TOKEN123', [
+ 'id' => 0,
+ 'uid' => 'this-is-the-events-uid',
+ 'recurrenceid' => null,
+ 'attendee' => 'mailto:attendee@foo.bar',
+ 'organizer' => 'mailto:organizer@foo.bar',
+ 'sequence' => null,
+ 'token' => 'TOKEN123',
+ 'expiration' => 420000,
+ ], 1337);
+
+ $expected = <<<EOF
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=DECLINED:mailto:attendee@foo.bar
+ORGANIZER:mailto:organizer@foo.bar
+UID:this-is-the-events-uid
+SEQUENCE:0
+REQUEST-STATUS:2.0;Success
+DTSTAMP:19700101T002217Z
+END:VEVENT
+END:VCALENDAR
+
+EOF;
+ $expected = preg_replace('~\R~u', "\r\n", $expected);
+
+ $called = false;
+ $this->responseServer->expects($this->once())
+ ->method('handleITipMessage')
+ ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void {
+ $called = true;
+ $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
+ $this->assertEquals('VEVENT', $iTipMessage->component);
+ $this->assertEquals('REPLY', $iTipMessage->method);
+ $this->assertEquals(null, $iTipMessage->sequence);
+ $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender);
+ if ($isExternalAttendee) {
+ $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient);
+ } else {
+ $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient);
+ }
+
+ $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
+
+ $this->assertEquals($expected, $iTipMessage->message->serialize());
+ });
+ $this->responseServer->expects($this->once())
+ ->method('isExternalAttendee')
+ ->willReturn($isExternalAttendee);
+
+ $response = $this->controller->decline('TOKEN123');
+ $this->assertInstanceOf(TemplateResponse::class, $response);
+ $this->assertEquals('schedule-response-success', $response->getTemplateName());
+ $this->assertEquals([], $response->getParams());
+ $this->assertTrue($called);
+ }
+
+ public function testOptions(): void {
+ $response = $this->controller->options('TOKEN123');
+ $this->assertInstanceOf(TemplateResponse::class, $response);
+ $this->assertEquals('schedule-response-options', $response->getTemplateName());
+ $this->assertEquals(['token' => 'TOKEN123'], $response->getParams());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')]
+ public function testProcessMoreOptionsResult(bool $isExternalAttendee): void {
+ $this->request->expects($this->once())
+ ->method('getParam')
+ ->with('partStat')
+ ->willReturn('TENTATIVE');
+
+ $this->buildQueryExpects('TOKEN123', [
+ 'id' => 0,
+ 'uid' => 'this-is-the-events-uid',
+ 'recurrenceid' => null,
+ 'attendee' => 'mailto:attendee@foo.bar',
+ 'organizer' => 'mailto:organizer@foo.bar',
+ 'sequence' => null,
+ 'token' => 'TOKEN123',
+ 'expiration' => 420000,
+ ], 1337);
+
+ $expected = <<<EOF
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:attendee@foo.bar
+ORGANIZER:mailto:organizer@foo.bar
+UID:this-is-the-events-uid
+SEQUENCE:0
+REQUEST-STATUS:2.0;Success
+DTSTAMP:19700101T002217Z
+END:VEVENT
+END:VCALENDAR
+
+EOF;
+ $expected = preg_replace('~\R~u', "\r\n", $expected);
+
+ $called = false;
+ $this->responseServer->expects($this->once())
+ ->method('handleITipMessage')
+ ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void {
+ $called = true;
+ $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
+ $this->assertEquals('VEVENT', $iTipMessage->component);
+ $this->assertEquals('REPLY', $iTipMessage->method);
+ $this->assertEquals(null, $iTipMessage->sequence);
+ $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender);
+ if ($isExternalAttendee) {
+ $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient);
+ } else {
+ $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient);
+ }
+
+ $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
+
+ $this->assertEquals($expected, $iTipMessage->message->serialize());
+ });
+ $this->responseServer->expects($this->once())
+ ->method('isExternalAttendee')
+ ->willReturn($isExternalAttendee);
+
+
+ $response = $this->controller->processMoreOptionsResult('TOKEN123');
+ $this->assertInstanceOf(TemplateResponse::class, $response);
+ $this->assertEquals('schedule-response-success', $response->getTemplateName());
+ $this->assertEquals([], $response->getParams());
+ $this->assertTrue($called);
+ }
+
+ private function buildQueryExpects(string $token, ?array $return, int $time): void {
+ $queryBuilder = $this->createMock(IQueryBuilder::class);
+ $stmt = $this->createMock(IResult::class);
+ $expr = $this->createMock(IExpressionBuilder::class);
+
+ $this->dbConnection->expects($this->once())
+ ->method('getQueryBuilder')
+ ->with()
+ ->willReturn($queryBuilder);
+ $queryBuilder->method('expr')
+ ->willReturn($expr);
+ $queryBuilder->method('createNamedParameter')
+ ->willReturnMap([
+ [$token, \PDO::PARAM_STR, null, 'namedParameterToken']
+ ]);
+
+ $stmt->expects($this->once())
+ ->method('fetch')
+ ->with(\PDO::FETCH_ASSOC)
+ ->willReturn($return);
+ $stmt->expects($this->once())
+ ->method('closeCursor');
+
+ $function = 'functionToken';
+ $expr->expects($this->once())
+ ->method('eq')
+ ->with('token', 'namedParameterToken')
+ ->willReturn((string)$function);
+
+ $this->dbConnection->expects($this->once())
+ ->method('getQueryBuilder')
+ ->with()
+ ->willReturn($queryBuilder);
+
+ $queryBuilder->expects($this->once())
+ ->method('select')
+ ->with('*')
+ ->willReturn($queryBuilder);
+ $queryBuilder->expects($this->once())
+ ->method('from')
+ ->with('calendar_invitations')
+ ->willReturn($queryBuilder);
+ $queryBuilder->expects($this->once())
+ ->method('where')
+ ->with($function)
+ ->willReturn($queryBuilder);
+ $queryBuilder->expects($this->once())
+ ->method('executeQuery')
+ ->with()
+ ->willReturn($stmt);
+
+ $this->timeFactory->method('getTime')
+ ->willReturn($time);
+ }
+}
diff --git a/apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php b/apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php
new file mode 100644
index 00000000000..527943e5221
--- /dev/null
+++ b/apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\DAV\Service;
+
+use OCA\DAV\CalDAV\UpcomingEvent;
+use OCA\DAV\CalDAV\UpcomingEventsService;
+use OCA\DAV\Controller\UpcomingEventsController;
+use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class UpcomingEventsControllerTest extends TestCase {
+ private IRequest&MockObject $request;
+ private UpcomingEventsService&MockObject $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->service = $this->createMock(UpcomingEventsService::class);
+ }
+
+ public function testGetEventsAnonymously(): void {
+ $controller = new UpcomingEventsController(
+ $this->request,
+ null,
+ $this->service,
+ );
+
+ $response = $controller->getEvents('https://cloud.example.com/call/123');
+
+ self::assertNull($response->getData());
+ self::assertSame(401, $response->getStatus());
+ }
+
+ public function testGetEventsByLocation(): void {
+ $controller = new UpcomingEventsController(
+ $this->request,
+ 'u1',
+ $this->service,
+ );
+ $this->service->expects(self::once())
+ ->method('getEvents')
+ ->with('u1', 'https://cloud.example.com/call/123')
+ ->willReturn([
+ new UpcomingEvent(
+ 'abc-123',
+ null,
+ 'personal',
+ 123,
+ 'Test',
+ 'https://cloud.example.com/call/123',
+ null,
+ ),
+ ]);
+
+ $response = $controller->getEvents('https://cloud.example.com/call/123');
+
+ self::assertNotNull($response->getData());
+ self::assertIsArray($response->getData());
+ self::assertCount(1, $response->getData()['events']);
+ self::assertSame(200, $response->getStatus());
+ $event1 = $response->getData()['events'][0];
+ self::assertEquals('abc-123', $event1['uri']);
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/AnonymousOptionsTest.php b/apps/dav/tests/unit/DAV/AnonymousOptionsTest.php
new file mode 100644
index 00000000000..c99ebf327c8
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/AnonymousOptionsTest.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV;
+
+use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
+use Sabre\DAV\Auth\Backend\BasicCallBack;
+use Sabre\DAV\Auth\Plugin;
+use Sabre\DAV\Server;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\HTTP\Sapi;
+use Test\TestCase;
+
+class AnonymousOptionsTest extends TestCase {
+ private function sendRequest(string $method, string $path, string $userAgent = '') {
+ $server = new Server();
+ $server->addPlugin(new AnonymousOptionsPlugin());
+ $server->addPlugin(new Plugin(new BasicCallBack(function () {
+ return false;
+ })));
+
+ $server->httpRequest->setMethod($method);
+ $server->httpRequest->setUrl($path);
+ $server->httpRequest->setHeader('User-Agent', $userAgent);
+
+ $server->sapi = new SapiMock();
+ $server->exec();
+ return $server->httpResponse;
+ }
+
+ public function testAnonymousOptionsRoot(): void {
+ $response = $this->sendRequest('OPTIONS', '');
+
+ $this->assertEquals(401, $response->getStatus());
+ }
+
+ public function testAnonymousOptionsNonRoot(): void {
+ $response = $this->sendRequest('OPTIONS', 'foo');
+
+ $this->assertEquals(401, $response->getStatus());
+ }
+
+ public function testAnonymousOptionsNonRootSubDir(): void {
+ $response = $this->sendRequest('OPTIONS', 'foo/bar');
+
+ $this->assertEquals(401, $response->getStatus());
+ }
+
+ public function testAnonymousOptionsRootOffice(): void {
+ $response = $this->sendRequest('OPTIONS', '', 'Microsoft Office does strange things');
+
+ $this->assertEquals(200, $response->getStatus());
+ }
+
+ public function testAnonymousOptionsNonRootOffice(): void {
+ $response = $this->sendRequest('OPTIONS', 'foo', 'Microsoft Office does strange things');
+
+ $this->assertEquals(200, $response->getStatus());
+ }
+
+ public function testAnonymousOptionsNonRootSubDirOffice(): void {
+ $response = $this->sendRequest('OPTIONS', 'foo/bar', 'Microsoft Office does strange things');
+
+ $this->assertEquals(200, $response->getStatus());
+ }
+
+ public function testAnonymousHead(): void {
+ $response = $this->sendRequest('HEAD', '', 'Microsoft Office does strange things');
+
+ $this->assertEquals(200, $response->getStatus());
+ }
+
+ public function testAnonymousHeadNoOffice(): void {
+ $response = $this->sendRequest('HEAD', '');
+
+ $this->assertEquals(401, $response->getStatus(), 'curl');
+ }
+}
+
+class SapiMock extends Sapi {
+ /**
+ * Overriding this so nothing is ever echo'd.
+ *
+ * @return void
+ */
+ public static function sendResponse(ResponseInterface $response): void {
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php b/apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php
new file mode 100644
index 00000000000..0e82ef0a3ae
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\DAV;
+
+use OCA\DAV\Files\BrowserErrorPagePlugin;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\HTTP\Response;
+
+class BrowserErrorPagePluginTest extends \Test\TestCase {
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesExceptions')]
+ public function test(int $expectedCode, \Throwable $exception): void {
+ /** @var BrowserErrorPagePlugin&MockObject $plugin */
+ $plugin = $this->getMockBuilder(BrowserErrorPagePlugin::class)->onlyMethods(['sendResponse', 'generateBody'])->getMock();
+ $plugin->expects($this->once())->method('generateBody')->willReturn(':boom:');
+ $plugin->expects($this->once())->method('sendResponse');
+ /** @var \Sabre\DAV\Server&MockObject $server */
+ $server = $this->createMock('Sabre\DAV\Server');
+ $server->expects($this->once())->method('on');
+ $httpResponse = $this->createMock(Response::class);
+ $httpResponse->expects($this->once())->method('addHeaders');
+ $httpResponse->expects($this->once())->method('setStatus')->with($expectedCode);
+ $httpResponse->expects($this->once())->method('setBody')->with(':boom:');
+ $server->httpResponse = $httpResponse;
+ $plugin->initialize($server);
+ $plugin->logException($exception);
+ }
+
+ public static function providesExceptions(): array {
+ return [
+ [ 404, new NotFound()],
+ [ 500, new \RuntimeException()],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php
new file mode 100644
index 00000000000..517969fc9a3
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php
@@ -0,0 +1,466 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV;
+
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\DAV\CustomPropertiesBackend;
+use OCA\DAV\Db\PropertyMapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\IUser;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\PropPatch;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\DAVACL\IACL;
+use Sabre\DAVACL\IPrincipal;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class CustomPropertiesBackendTest extends TestCase {
+ private const BASE_URI = '/remote.php/dav/';
+
+ private Server&MockObject $server;
+ private Tree&MockObject $tree;
+ private IDBConnection $dbConnection;
+ private IUser&MockObject $user;
+ private DefaultCalendarValidator&MockObject $defaultCalendarValidator;
+ private CustomPropertiesBackend $backend;
+ private PropertyMapper $propertyMapper;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->server = $this->createMock(Server::class);
+ $this->server->method('getBaseUri')
+ ->willReturn(self::BASE_URI);
+ $this->tree = $this->createMock(Tree::class);
+ $this->user = $this->createMock(IUser::class);
+ $this->user->method('getUID')
+ ->with()
+ ->willReturn('dummy_user_42');
+ $this->dbConnection = \OCP\Server::get(IDBConnection::class);
+ $this->propertyMapper = \OCP\Server::get(PropertyMapper::class);
+ $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class);
+
+ $this->backend = new CustomPropertiesBackend(
+ $this->server,
+ $this->tree,
+ $this->dbConnection,
+ $this->user,
+ $this->propertyMapper,
+ $this->defaultCalendarValidator,
+ );
+ }
+
+ protected function tearDown(): void {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->delete('properties');
+ $query->execute();
+
+ parent::tearDown();
+ }
+
+ private function formatPath(string $path): string {
+ if (strlen($path) > 250) {
+ return sha1($path);
+ } else {
+ return $path;
+ }
+ }
+
+ protected function insertProps(string $user, string $path, array $props): void {
+ foreach ($props as $name => $value) {
+ $this->insertProp($user, $path, $name, $value);
+ }
+ }
+
+ protected function insertProp(string $user, string $path, string $name, mixed $value): void {
+ $type = CustomPropertiesBackend::PROPERTY_TYPE_STRING;
+ if ($value instanceof Href) {
+ $value = $value->getHref();
+ $type = CustomPropertiesBackend::PROPERTY_TYPE_HREF;
+ }
+
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->insert('properties')
+ ->values([
+ 'userid' => $query->createNamedParameter($user),
+ 'propertypath' => $query->createNamedParameter($this->formatPath($path)),
+ 'propertyname' => $query->createNamedParameter($name),
+ 'propertyvalue' => $query->createNamedParameter($value),
+ 'valuetype' => $query->createNamedParameter($type, IQueryBuilder::PARAM_INT)
+ ]);
+ $query->execute();
+ }
+
+ protected function getProps(string $user, string $path): array {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->select('propertyname', 'propertyvalue', 'valuetype')
+ ->from('properties')
+ ->where($query->expr()->eq('userid', $query->createNamedParameter($user)))
+ ->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($this->formatPath($path))));
+
+ $result = $query->execute();
+ $data = [];
+ while ($row = $result->fetch()) {
+ $value = $row['propertyvalue'];
+ if ((int)$row['valuetype'] === CustomPropertiesBackend::PROPERTY_TYPE_HREF) {
+ $value = new Href($value);
+ }
+ $data[$row['propertyname']] = $value;
+ }
+ $result->closeCursor();
+
+ return $data;
+ }
+
+ public function testPropFindNoDbCalls(): void {
+ $db = $this->createMock(IDBConnection::class);
+ $backend = new CustomPropertiesBackend(
+ $this->server,
+ $this->tree,
+ $db,
+ $this->user,
+ $this->propertyMapper,
+ $this->defaultCalendarValidator,
+ );
+
+ $propFind = $this->createMock(PropFind::class);
+ $propFind->expects($this->once())
+ ->method('get404Properties')
+ ->with()
+ ->willReturn([
+ '{http://owncloud.org/ns}permissions',
+ '{http://owncloud.org/ns}downloadURL',
+ '{http://owncloud.org/ns}dDC',
+ '{http://owncloud.org/ns}size',
+ ]);
+
+ $db->expects($this->never())
+ ->method($this->anything());
+
+ $backend->propFind('foo_bar_path_1337_0', $propFind);
+ }
+
+ public function testPropFindCalendarCall(): void {
+ $propFind = $this->createMock(PropFind::class);
+ $propFind->method('get404Properties')
+ ->with()
+ ->willReturn([
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ '{abc}def',
+ ]);
+
+ $propFind->method('getRequestedProperties')
+ ->with()
+ ->willReturn([
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ '{DAV:}displayname',
+ '{urn:ietf:params:xml:ns:caldav}calendar-description',
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone',
+ '{abc}def',
+ ]);
+
+ $props = [
+ '{abc}def' => 'a',
+ '{DAV:}displayname' => 'b',
+ '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'c',
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'd',
+ ];
+
+ $this->insertProps('dummy_user_42', 'calendars/foo/bar_path_1337_0', $props);
+
+ $setProps = [];
+ $propFind->method('set')
+ ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
+ $setProps[$name] = $value;
+ });
+
+ $this->backend->propFind('calendars/foo/bar_path_1337_0', $propFind);
+ $this->assertEquals($props, $setProps);
+ }
+
+ public function testPropFindPrincipalCall(): void {
+ $this->tree->method('getNodeForPath')
+ ->willReturnCallback(function ($uri) {
+ $node = $this->createMock(Calendar::class);
+ $node->method('getOwner')
+ ->willReturn('principals/users/dummy_user_42');
+ return $node;
+ });
+
+ $propFind = $this->createMock(PropFind::class);
+ $propFind->method('get404Properties')
+ ->with()
+ ->willReturn([
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ '{abc}def',
+ ]);
+
+ $propFind->method('getRequestedProperties')
+ ->with()
+ ->willReturn([
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ '{abc}def',
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
+ ]);
+
+ $props = [
+ '{abc}def' => 'a',
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/admin/personal'),
+ ];
+ $this->insertProps('dummy_user_42', 'principals/users/dummy_user_42', $props);
+
+ $setProps = [];
+ $propFind->method('set')
+ ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
+ $setProps[$name] = $value;
+ });
+
+ $this->backend->propFind('principals/users/dummy_user_42', $propFind);
+ $this->assertEquals($props, $setProps);
+ }
+
+ public static function propFindPrincipalScheduleDefaultCalendarProviderUrlProvider(): array {
+ // [ user, nodes, existingProps, requestedProps, returnedProps ]
+ return [
+ [ // Exists
+ 'dummy_user_42',
+ ['calendars/dummy_user_42/foo/' => Calendar::class],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')],
+ ],
+ [ // Doesn't exist
+ 'dummy_user_42',
+ ['calendars/dummy_user_42/foo/' => Calendar::class],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/bar/')],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+ [],
+ ],
+ [ // No privilege
+ 'dummy_user_42',
+ ['calendars/user2/baz/' => Calendar::class],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/user2/baz/')],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+ [],
+ ],
+ [ // Not a calendar
+ 'dummy_user_42',
+ ['foo/dummy_user_42/bar/' => IACL::class],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/dummy_user_42/bar/')],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+ [],
+ ],
+ ];
+
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('propFindPrincipalScheduleDefaultCalendarProviderUrlProvider')]
+ public function testPropFindPrincipalScheduleDefaultCalendarUrl(
+ string $user,
+ array $nodes,
+ array $existingProps,
+ array $requestedProps,
+ array $returnedProps,
+ ): void {
+ $propFind = $this->createMock(PropFind::class);
+ $propFind->method('get404Properties')
+ ->with()
+ ->willReturn([
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ ]);
+
+ $propFind->method('getRequestedProperties')
+ ->with()
+ ->willReturn(array_merge([
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ '{abc}def',
+ ],
+ $requestedProps,
+ ));
+
+ $this->server->method('calculateUri')
+ ->willReturnCallback(function ($uri) {
+ if (!str_starts_with($uri, self::BASE_URI)) {
+ return trim(substr($uri, strlen(self::BASE_URI)), '/');
+ }
+ return null;
+ });
+ $this->tree->method('getNodeForPath')
+ ->willReturnCallback(function ($uri) use ($nodes) {
+ if (str_starts_with($uri, 'principals/')) {
+ return $this->createMock(IPrincipal::class);
+ }
+ if (array_key_exists($uri, $nodes)) {
+ $owner = explode('/', $uri)[1];
+ $node = $this->createMock($nodes[$uri]);
+ $node->method('getOwner')
+ ->willReturn("principals/users/$owner");
+ return $node;
+ }
+ throw new NotFound('Node not found');
+ });
+
+ $this->insertProps($user, "principals/users/$user", $existingProps);
+
+ $setProps = [];
+ $propFind->method('set')
+ ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
+ $setProps[$name] = $value;
+ });
+
+ $this->backend->propFind("principals/users/$user", $propFind);
+ $this->assertEquals($returnedProps, $setProps);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('propPatchProvider')]
+ public function testPropPatch(string $path, array $existing, array $props, array $result): void {
+ $this->server->method('calculateUri')
+ ->willReturnCallback(function ($uri) {
+ if (str_starts_with($uri, self::BASE_URI)) {
+ return trim(substr($uri, strlen(self::BASE_URI)), '/');
+ }
+ return null;
+ });
+ $this->tree->method('getNodeForPath')
+ ->willReturnCallback(function ($uri) {
+ $node = $this->createMock(Calendar::class);
+ $node->method('getOwner')
+ ->willReturn('principals/users/' . $this->user->getUID());
+ return $node;
+ });
+
+ $this->insertProps($this->user->getUID(), $path, $existing);
+ $propPatch = new PropPatch($props);
+
+ $this->backend->propPatch($path, $propPatch);
+ $propPatch->commit();
+
+ $storedProps = $this->getProps($this->user->getUID(), $path);
+ $this->assertEquals($result, $storedProps);
+ }
+
+ public static function propPatchProvider(): array {
+ $longPath = str_repeat('long_path', 100);
+ return [
+ ['foo_bar_path_1337', [], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
+ ['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
+ ['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => null], []],
+ [$longPath, [], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
+ ['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]],
+ ['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href(self::BASE_URI . 'foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]],
+ ];
+ }
+
+ public function testPropPatchWithUnsuitableCalendar(): void {
+ $path = 'principals/users/' . $this->user->getUID();
+
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('getOwner')
+ ->willReturn($path);
+
+ $this->defaultCalendarValidator->expects(self::once())
+ ->method('validateScheduleDefaultCalendar')
+ ->with($node)
+ ->willThrowException(new \Sabre\DAV\Exception('Invalid calendar'));
+
+ $this->server->method('calculateUri')
+ ->willReturnCallback(function ($uri) {
+ if (str_starts_with($uri, self::BASE_URI)) {
+ return trim(substr($uri, strlen(self::BASE_URI)), '/');
+ }
+ return null;
+ });
+ $this->tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('foo/bar/')
+ ->willReturn($node);
+
+ $storedProps = $this->getProps($this->user->getUID(), $path);
+ $this->assertEquals([], $storedProps);
+
+ $propPatch = new PropPatch([
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/'),
+ ]);
+ $this->backend->propPatch($path, $propPatch);
+ try {
+ $propPatch->commit();
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(\Sabre\DAV\Exception::class, $e);
+ }
+
+ $storedProps = $this->getProps($this->user->getUID(), $path);
+ $this->assertEquals([], $storedProps);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('deleteProvider')]
+ public function testDelete(string $path): void {
+ $this->insertProps('dummy_user_42', $path, ['foo' => 'bar']);
+ $this->backend->delete($path);
+ $this->assertEquals([], $this->getProps('dummy_user_42', $path));
+ }
+
+ public static function deleteProvider(): array {
+ return [
+ ['foo_bar_path_1337'],
+ [str_repeat('long_path', 100)]
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('moveProvider')]
+ public function testMove(string $source, string $target): void {
+ $this->insertProps('dummy_user_42', $source, ['foo' => 'bar']);
+ $this->backend->move($source, $target);
+ $this->assertEquals([], $this->getProps('dummy_user_42', $source));
+ $this->assertEquals(['foo' => 'bar'], $this->getProps('dummy_user_42', $target));
+ }
+
+ public static function moveProvider(): array {
+ return [
+ ['foo_bar_path_1337', 'foo_bar_path_7333'],
+ [str_repeat('long_path1', 100), str_repeat('long_path2', 100)]
+ ];
+ }
+
+ public function testDecodeValueFromDatabaseObjectCurrent(): void {
+ $propertyValue = 'O:48:"Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp":1:{s:8:"\x00*\x00value";s:6:"opaque";}';
+ $propertyType = 3;
+ $decodeValue = $this->invokePrivate($this->backend, 'decodeValueFromDatabase', [$propertyValue, $propertyType]);
+ $this->assertInstanceOf(\Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp::class, $decodeValue);
+ $this->assertEquals('opaque', $decodeValue->getValue());
+ }
+
+ public function testDecodeValueFromDatabaseObjectLegacy(): void {
+ $propertyValue = 'O:48:"Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp":1:{s:8:"' . chr(0) . '*' . chr(0) . 'value";s:6:"opaque";}';
+ $propertyType = 3;
+ $decodeValue = $this->invokePrivate($this->backend, 'decodeValueFromDatabase', [$propertyValue, $propertyType]);
+ $this->assertInstanceOf(\Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp::class, $decodeValue);
+ $this->assertEquals('opaque', $decodeValue->getValue());
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/GroupPrincipalTest.php b/apps/dav/tests/unit/DAV/GroupPrincipalTest.php
new file mode 100644
index 00000000000..2756152a6e2
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/GroupPrincipalTest.php
@@ -0,0 +1,331 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\DAV;
+
+use OC\Group\Group;
+use OCA\DAV\DAV\GroupPrincipalBackend;
+use OCP\IConfig;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Share\IManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\PropPatch;
+
+class GroupPrincipalTest extends \Test\TestCase {
+ private IConfig&MockObject $config;
+ private IGroupManager&MockObject $groupManager;
+ private IUserSession&MockObject $userSession;
+ private IManager&MockObject $shareManager;
+ private GroupPrincipalBackend $connector;
+
+ protected function setUp(): void {
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->shareManager = $this->createMock(IManager::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $this->connector = new GroupPrincipalBackend(
+ $this->groupManager,
+ $this->userSession,
+ $this->shareManager,
+ $this->config
+ );
+ parent::setUp();
+ }
+
+ public function testGetPrincipalsByPrefixWithoutPrefix(): void {
+ $response = $this->connector->getPrincipalsByPrefix('');
+ $this->assertSame([], $response);
+ }
+
+ public function testGetPrincipalsByPrefixWithUsers(): void {
+ $group1 = $this->mockGroup('foo');
+ $group2 = $this->mockGroup('bar');
+ $this->groupManager
+ ->expects($this->once())
+ ->method('search')
+ ->with('')
+ ->willReturn([$group1, $group2]);
+
+ $expectedResponse = [
+ 0 => [
+ 'uri' => 'principals/groups/foo',
+ '{DAV:}displayname' => 'Group foo',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP',
+ ],
+ 1 => [
+ 'uri' => 'principals/groups/bar',
+ '{DAV:}displayname' => 'Group bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP',
+ ]
+ ];
+ $response = $this->connector->getPrincipalsByPrefix('principals/groups');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetPrincipalsByPrefixEmpty(): void {
+ $this->groupManager
+ ->expects($this->once())
+ ->method('search')
+ ->with('')
+ ->willReturn([]);
+
+ $response = $this->connector->getPrincipalsByPrefix('principals/groups');
+ $this->assertSame([], $response);
+ }
+
+ public function testGetPrincipalsByPathWithoutMail(): void {
+ $group1 = $this->mockGroup('foo');
+ $this->groupManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willReturn($group1);
+
+ $expectedResponse = [
+ 'uri' => 'principals/groups/foo',
+ '{DAV:}displayname' => 'Group foo',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP',
+ ];
+ $response = $this->connector->getPrincipalByPath('principals/groups/foo');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetPrincipalsByPathWithMail(): void {
+ $fooUser = $this->mockGroup('foo');
+ $this->groupManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willReturn($fooUser);
+
+ $expectedResponse = [
+ 'uri' => 'principals/groups/foo',
+ '{DAV:}displayname' => 'Group foo',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP',
+ ];
+ $response = $this->connector->getPrincipalByPath('principals/groups/foo');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetPrincipalsByPathEmpty(): void {
+ $this->groupManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willReturn(null);
+
+ $response = $this->connector->getPrincipalByPath('principals/groups/foo');
+ $this->assertSame(null, $response);
+ }
+
+ public function testGetPrincipalsByPathGroupWithSlash(): void {
+ $group1 = $this->mockGroup('foo/bar');
+ $this->groupManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo/bar')
+ ->willReturn($group1);
+
+ $expectedResponse = [
+ 'uri' => 'principals/groups/foo%2Fbar',
+ '{DAV:}displayname' => 'Group foo/bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP',
+ ];
+ $response = $this->connector->getPrincipalByPath('principals/groups/foo/bar');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetPrincipalsByPathGroupWithHash(): void {
+ $group1 = $this->mockGroup('foo#bar');
+ $this->groupManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo#bar')
+ ->willReturn($group1);
+
+ $expectedResponse = [
+ 'uri' => 'principals/groups/foo%23bar',
+ '{DAV:}displayname' => 'Group foo#bar',
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP',
+ ];
+ $response = $this->connector->getPrincipalByPath('principals/groups/foo#bar');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetGroupMemberSet(): void {
+ $response = $this->connector->getGroupMemberSet('principals/groups/foo');
+ $this->assertSame([], $response);
+ }
+
+ public function testGetGroupMembership(): void {
+ $response = $this->connector->getGroupMembership('principals/groups/foo');
+ $this->assertSame([], $response);
+ }
+
+
+ public function testSetGroupMembership(): void {
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->expectExceptionMessage('Setting members of the group is not supported yet');
+
+ $this->connector->setGroupMemberSet('principals/groups/foo', ['foo']);
+ }
+
+ public function testUpdatePrincipal(): void {
+ $this->assertSame(0, $this->connector->updatePrincipal('foo', new PropPatch([])));
+ }
+
+ public function testSearchPrincipalsWithEmptySearchProperties(): void {
+ $this->assertSame([], $this->connector->searchPrincipals('principals/groups', []));
+ }
+
+ public function testSearchPrincipalsWithWrongPrefixPath(): void {
+ $this->assertSame([], $this->connector->searchPrincipals('principals/users',
+ ['{DAV:}displayname' => 'Foo']));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('searchPrincipalsDataProvider')]
+ public function testSearchPrincipals(bool $sharingEnabled, bool $groupSharingEnabled, bool $groupsOnly, string $test, array $result): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareAPIEnabled')
+ ->willReturn($sharingEnabled);
+
+ $this->shareManager->expects($sharingEnabled ? $this->once() : $this->never())
+ ->method('allowGroupSharing')
+ ->willReturn($groupSharingEnabled);
+
+ if ($sharingEnabled && $groupSharingEnabled) {
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn($groupsOnly);
+
+ if ($groupsOnly) {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2', 'group5']);
+ }
+ } else {
+ $this->shareManager->expects($this->never())
+ ->method('shareWithGroupMembersOnly');
+ $this->groupManager->expects($this->never())
+ ->method($this->anything());
+ }
+
+ $group1 = $this->createMock(IGroup::class);
+ $group1->method('getGID')->willReturn('group1');
+ $group2 = $this->createMock(IGroup::class);
+ $group2->method('getGID')->willReturn('group2');
+ $group3 = $this->createMock(IGroup::class);
+ $group3->method('getGID')->willReturn('group3');
+ $group4 = $this->createMock(IGroup::class);
+ $group4->method('getGID')->willReturn('group4');
+ $group5 = $this->createMock(IGroup::class);
+ $group5->method('getGID')->willReturn('group5');
+
+ if ($sharingEnabled && $groupSharingEnabled) {
+ $this->groupManager->expects($this->once())
+ ->method('search')
+ ->with('Foo')
+ ->willReturn([$group1, $group2, $group3, $group4, $group5]);
+ } else {
+ $this->groupManager->expects($this->never())
+ ->method('search');
+ }
+
+ $this->assertSame($result, $this->connector->searchPrincipals('principals/groups',
+ ['{DAV:}displayname' => 'Foo'], $test));
+ }
+
+ public static function searchPrincipalsDataProvider(): array {
+ return [
+ [true, true, false, 'allof', ['principals/groups/group1', 'principals/groups/group2', 'principals/groups/group3', 'principals/groups/group4', 'principals/groups/group5']],
+ [true, true, false, 'anyof', ['principals/groups/group1', 'principals/groups/group2', 'principals/groups/group3', 'principals/groups/group4', 'principals/groups/group5']],
+ [true, true, true, 'allof', ['principals/groups/group1', 'principals/groups/group2', 'principals/groups/group5']],
+ [true, true, true, 'anyof', ['principals/groups/group1', 'principals/groups/group2', 'principals/groups/group5']],
+ [true, false, false, 'allof', []],
+ [false, true, false, 'anyof', []],
+ [false, false, false, 'allof', []],
+ [false, false, false, 'anyof', []],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('findByUriDataProvider')]
+ public function testFindByUri(bool $sharingEnabled, bool $groupSharingEnabled, bool $groupsOnly, string $findUri, ?string $result): void {
+ $this->shareManager->expects($this->once())
+ ->method('shareAPIEnabled')
+ ->willReturn($sharingEnabled);
+
+ $this->shareManager->expects($sharingEnabled ? $this->once() : $this->never())
+ ->method('allowGroupSharing')
+ ->willReturn($groupSharingEnabled);
+
+ if ($sharingEnabled && $groupSharingEnabled) {
+ $this->shareManager->expects($this->once())
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn($groupsOnly);
+
+ if ($groupsOnly) {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->groupManager->expects($this->once())
+ ->method('getUserGroupIds')
+ ->with($user)
+ ->willReturn(['group1', 'group2', 'group5']);
+ }
+ } else {
+ $this->shareManager->expects($this->never())
+ ->method('shareWithGroupMembersOnly');
+ $this->groupManager->expects($this->never())
+ ->method($this->anything());
+ }
+
+ $this->assertEquals($result, $this->connector->findByUri($findUri, 'principals/groups'));
+ }
+
+ public static function findByUriDataProvider(): array {
+ return [
+ [false, false, false, 'principal:principals/groups/group1', null],
+ [false, false, false, 'principal:principals/groups/group3', null],
+ [false, true, false, 'principal:principals/groups/group1', null],
+ [false, true, false, 'principal:principals/groups/group3', null],
+ [false, false, true, 'principal:principals/groups/group1', null],
+ [false, false, true, 'principal:principals/groups/group3', null],
+ [true, false, true, 'principal:principals/groups/group1', null],
+ [true, false, true, 'principal:principals/groups/group3', null],
+ [true, true, true, 'principal:principals/groups/group1', 'principals/groups/group1'],
+ [true, true, true, 'principal:principals/groups/group3', null],
+ [true, true, false, 'principal:principals/groups/group1', 'principals/groups/group1'],
+ [true, true, false, 'principal:principals/groups/group3', 'principals/groups/group3'],
+ ];
+ }
+
+ private function mockGroup(string $gid): Group&MockObject {
+ $fooGroup = $this->createMock(Group::class);
+ $fooGroup
+ ->expects($this->exactly(1))
+ ->method('getGID')
+ ->willReturn($gid);
+ $fooGroup
+ ->expects($this->exactly(1))
+ ->method('getDisplayName')
+ ->willReturn('Group ' . $gid);
+ return $fooGroup;
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php b/apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php
new file mode 100644
index 00000000000..8e410eb0a78
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php
@@ -0,0 +1,183 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Tests\unit\DAV\Listener;
+
+use OCA\DAV\BackgroundJob\UserStatusAutomation;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\SyncService;
+use OCA\DAV\Listener\UserEventsListener;
+use OCA\DAV\Service\ExampleContactService;
+use OCA\DAV\Service\ExampleEventService;
+use OCP\BackgroundJob\IJobList;
+use OCP\Defaults;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class UserEventsListenerTest extends TestCase {
+ private IUserManager&MockObject $userManager;
+ private SyncService&MockObject $syncService;
+ private CalDavBackend&MockObject $calDavBackend;
+ private CardDavBackend&MockObject $cardDavBackend;
+ private Defaults&MockObject $defaults;
+ private ExampleContactService&MockObject $exampleContactService;
+ private ExampleEventService&MockObject $exampleEventService;
+ private LoggerInterface&MockObject $logger;
+
+ private UserEventsListener $userEventsListener;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->syncService = $this->createMock(SyncService::class);
+ $this->calDavBackend = $this->createMock(CalDavBackend::class);
+ $this->cardDavBackend = $this->createMock(CardDavBackend::class);
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->exampleContactService = $this->createMock(ExampleContactService::class);
+ $this->exampleEventService = $this->createMock(ExampleEventService::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->jobList = $this->createMock(IJobList::class);
+
+ $this->userEventsListener = new UserEventsListener(
+ $this->userManager,
+ $this->syncService,
+ $this->calDavBackend,
+ $this->cardDavBackend,
+ $this->defaults,
+ $this->exampleContactService,
+ $this->exampleEventService,
+ $this->logger,
+ $this->jobList,
+ );
+ }
+
+ public function test(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())->method('getUID')->willReturn('newUser');
+
+ $this->defaults->expects($this->once())->method('getColorPrimary')->willReturn('#745bca');
+
+ $this->calDavBackend->expects($this->once())->method('getCalendarsForUserCount')->willReturn(0);
+ $this->calDavBackend->expects($this->once())->method('createCalendar')->with(
+ 'principals/users/newUser',
+ 'personal', [
+ '{DAV:}displayname' => 'Personal',
+ '{http://apple.com/ns/ical/}calendar-color' => '#745bca',
+ 'components' => 'VEVENT'
+ ])
+ ->willReturn(1000);
+ $this->calDavBackend->expects(self::never())
+ ->method('getCalendarsForUser');
+ $this->exampleEventService->expects(self::once())
+ ->method('createExampleEvent')
+ ->with(1000);
+
+ $this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(0);
+ $this->cardDavBackend->expects($this->once())->method('createAddressBook')->with(
+ 'principals/users/newUser',
+ 'contacts', ['{DAV:}displayname' => 'Contacts']);
+
+ $this->userEventsListener->firstLogin($user);
+ }
+
+ public function testWithExisting(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())->method('getUID')->willReturn('newUser');
+
+ $this->calDavBackend->expects($this->once())->method('getCalendarsForUserCount')->willReturn(1);
+ $this->calDavBackend->expects($this->never())->method('createCalendar');
+ $this->calDavBackend->expects(self::never())
+ ->method('createCalendar');
+ $this->exampleEventService->expects(self::never())
+ ->method('createExampleEvent');
+
+ $this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(1);
+ $this->cardDavBackend->expects($this->never())->method('createAddressBook');
+
+ $this->userEventsListener->firstLogin($user);
+ }
+
+ public function testWithBirthdayCalendar(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())->method('getUID')->willReturn('newUser');
+
+ $this->defaults->expects($this->once())->method('getColorPrimary')->willReturn('#745bca');
+
+ $this->calDavBackend->expects($this->once())->method('getCalendarsForUserCount')->willReturn(0);
+ $this->calDavBackend->expects($this->once())->method('createCalendar')->with(
+ 'principals/users/newUser',
+ 'personal', [
+ '{DAV:}displayname' => 'Personal',
+ '{http://apple.com/ns/ical/}calendar-color' => '#745bca',
+ 'components' => 'VEVENT'
+ ]);
+
+ $this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(0);
+ $this->cardDavBackend->expects($this->once())->method('createAddressBook')->with(
+ 'principals/users/newUser',
+ 'contacts', ['{DAV:}displayname' => 'Contacts']);
+
+ $this->userEventsListener->firstLogin($user);
+ }
+
+ public function testDeleteCalendar(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())->method('getUID')->willReturn('newUser');
+
+ $this->syncService->expects($this->once())
+ ->method('deleteUser');
+
+ $this->calDavBackend->expects($this->once())->method('getUsersOwnCalendars')->willReturn([
+ ['id' => 'personal']
+ ]);
+ $this->calDavBackend->expects($this->once())->method('getSubscriptionsForUser')->willReturn([
+ ['id' => 'some-subscription']
+ ]);
+ $this->calDavBackend->expects($this->once())->method('deleteCalendar')->with('personal');
+ $this->calDavBackend->expects($this->once())->method('deleteSubscription')->with('some-subscription');
+ $this->calDavBackend->expects($this->once())->method('deleteAllSharesByUser');
+
+ $this->cardDavBackend->expects($this->once())->method('getUsersOwnAddressBooks')->willReturn([
+ ['id' => 'personal']
+ ]);
+ $this->cardDavBackend->expects($this->once())->method('deleteAddressBook');
+
+ $this->userEventsListener->preDeleteUser($user);
+ $this->userEventsListener->postDeleteUser('newUser');
+ }
+
+ public function testDeleteUserAutomationEvent(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())->method('getUID')->willReturn('newUser');
+
+ $this->syncService->expects($this->once())
+ ->method('deleteUser');
+
+ $this->calDavBackend->expects($this->once())->method('getUsersOwnCalendars')->willReturn([
+ ['id' => []]
+ ]);
+ $this->calDavBackend->expects($this->once())->method('getSubscriptionsForUser')->willReturn([
+ ['id' => []]
+ ]);
+ $this->cardDavBackend->expects($this->once())->method('getUsersOwnAddressBooks')->willReturn([
+ ['id' => []]
+ ]);
+
+ $this->jobList->expects(self::once())->method('remove')->with(UserStatusAutomation::class, ['userId' => 'newUser']);
+
+ $this->userEventsListener->preDeleteUser($user);
+ $this->userEventsListener->postDeleteUser('newUser');
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/Sharing/BackendTest.php b/apps/dav/tests/unit/DAV/Sharing/BackendTest.php
new file mode 100644
index 00000000000..556a623a73f
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/Sharing/BackendTest.php
@@ -0,0 +1,399 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV\Sharing;
+
+use OCA\DAV\CalDAV\Sharing\Backend as CalendarSharingBackend;
+use OCA\DAV\CalDAV\Sharing\Service;
+use OCA\DAV\CardDAV\Sharing\Backend as ContactsSharingBackend;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\Backend;
+use OCA\DAV\DAV\Sharing\IShareable;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class BackendTest extends TestCase {
+
+ private IDBConnection&MockObject $db;
+ private IUserManager&MockObject $userManager;
+ private IGroupManager&MockObject $groupManager;
+ private Principal&MockObject $principalBackend;
+ private ICache&MockObject $shareCache;
+ private LoggerInterface&MockObject $logger;
+ private ICacheFactory&MockObject $cacheFactory;
+ private Service&MockObject $calendarService;
+ private CalendarSharingBackend $backend;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->db = $this->createMock(IDBConnection::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->principalBackend = $this->createMock(Principal::class);
+ $this->cacheFactory = $this->createMock(ICacheFactory::class);
+ $this->shareCache = $this->createMock(ICache::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->calendarService = $this->createMock(Service::class);
+ $this->cacheFactory->expects(self::any())
+ ->method('createInMemory')
+ ->willReturn($this->shareCache);
+
+ $this->backend = new CalendarSharingBackend(
+ $this->userManager,
+ $this->groupManager,
+ $this->principalBackend,
+ $this->cacheFactory,
+ $this->calendarService,
+ $this->logger,
+ );
+ }
+
+ public function testUpdateShareCalendarBob(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $add = [
+ [
+ 'href' => 'principal:principals/users/bob',
+ 'readOnly' => true,
+ ]
+ ];
+ $principal = 'principals/users/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->userManager->expects(self::once())
+ ->method('userExists')
+ ->willReturn(true);
+ $this->groupManager->expects(self::never())
+ ->method('groupExists');
+ $this->calendarService->expects(self::once())
+ ->method('shareWith')
+ ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ);
+
+ $this->backend->updateShares($shareable, $add, []);
+ }
+
+ public function testUpdateShareCalendarGroup(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $add = [
+ [
+ 'href' => 'principal:principals/groups/bob',
+ 'readOnly' => true,
+ ]
+ ];
+ $principal = 'principals/groups/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->userManager->expects(self::never())
+ ->method('userExists');
+ $this->groupManager->expects(self::once())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->calendarService->expects(self::once())
+ ->method('shareWith')
+ ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ);
+
+ $this->backend->updateShares($shareable, $add, []);
+ }
+
+ public function testUpdateShareContactsBob(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $add = [
+ [
+ 'href' => 'principal:principals/users/bob',
+ 'readOnly' => true,
+ ]
+ ];
+ $principal = 'principals/users/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->userManager->expects(self::once())
+ ->method('userExists')
+ ->willReturn(true);
+ $this->groupManager->expects(self::never())
+ ->method('groupExists');
+ $this->calendarService->expects(self::once())
+ ->method('shareWith')
+ ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ);
+
+ $this->backend->updateShares($shareable, $add, []);
+ }
+
+ public function testUpdateShareContactsGroup(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $add = [
+ [
+ 'href' => 'principal:principals/groups/bob',
+ 'readOnly' => true,
+ ]
+ ];
+ $principal = 'principals/groups/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->userManager->expects(self::never())
+ ->method('userExists');
+ $this->groupManager->expects(self::once())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->calendarService->expects(self::once())
+ ->method('shareWith')
+ ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ);
+
+ $this->backend->updateShares($shareable, $add, []);
+ }
+
+ public function testUpdateShareCircle(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $add = [
+ [
+ 'href' => 'principal:principals/circles/bob',
+ 'readOnly' => true,
+ ]
+ ];
+ $principal = 'principals/groups/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->userManager->expects(self::never())
+ ->method('userExists');
+ $this->groupManager->expects(self::once())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->calendarService->expects(self::once())
+ ->method('shareWith')
+ ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ);
+
+ $this->backend->updateShares($shareable, $add, []);
+ }
+
+ public function testUnshareBob(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $remove = [
+ 'principal:principals/users/bob',
+ ];
+ $principal = 'principals/users/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->calendarService->expects(self::once())
+ ->method('deleteShare')
+ ->with($shareable->getResourceId(), $principal);
+ $this->calendarService->expects(self::never())
+ ->method('unshare');
+
+ $this->backend->updateShares($shareable, [], $remove);
+ }
+
+ public function testUnshareWithBobGroup(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $remove = [
+ 'principal:principals/users/bob',
+ ];
+ $oldShares = [
+ [
+ 'href' => 'principal:principals/groups/bob',
+ 'commonName' => 'bob',
+ 'status' => 1,
+ 'readOnly' => true,
+ '{http://owncloud.org/ns}principal' => 'principals/groups/bob',
+ '{http://owncloud.org/ns}group-share' => true,
+ ]
+ ];
+
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn('principals/users/bob');
+ $this->calendarService->expects(self::once())
+ ->method('deleteShare')
+ ->with($shareable->getResourceId(), 'principals/users/bob');
+ $this->calendarService->expects(self::never())
+ ->method('unshare');
+
+ $this->backend->updateShares($shareable, [], $remove, $oldShares);
+ }
+
+ public function testGetShares(): void {
+ $resourceId = 42;
+ $principal = 'principals/groups/bob';
+ $rows = [
+ [
+ 'principaluri' => $principal,
+ 'access' => Backend::ACCESS_READ,
+ ]
+ ];
+ $expected = [
+ [
+ 'href' => 'principal:principals/groups/bob',
+ 'commonName' => 'bob',
+ 'status' => 1,
+ 'readOnly' => true,
+ '{http://owncloud.org/ns}principal' => $principal,
+ '{http://owncloud.org/ns}group-share' => true,
+ ]
+ ];
+
+
+ $this->shareCache->expects(self::once())
+ ->method('get')
+ ->with((string)$resourceId)
+ ->willReturn(null);
+ $this->calendarService->expects(self::once())
+ ->method('getShares')
+ ->with($resourceId)
+ ->willReturn($rows);
+ $this->principalBackend->expects(self::once())
+ ->method('getPrincipalByPath')
+ ->with($principal)
+ ->willReturn(['uri' => $principal, '{DAV:}displayname' => 'bob']);
+ $this->shareCache->expects(self::once())
+ ->method('set')
+ ->with((string)$resourceId, $expected);
+
+ $result = $this->backend->getShares($resourceId);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testGetSharesAddressbooks(): void {
+ $service = $this->createMock(\OCA\DAV\CardDAV\Sharing\Service::class);
+ $backend = new ContactsSharingBackend(
+ $this->userManager,
+ $this->groupManager,
+ $this->principalBackend,
+ $this->cacheFactory,
+ $service,
+ $this->logger);
+ $resourceId = 42;
+ $principal = 'principals/groups/bob';
+ $rows = [
+ [
+ 'principaluri' => $principal,
+ 'access' => Backend::ACCESS_READ,
+ ]
+ ];
+ $expected = [
+ [
+ 'href' => 'principal:principals/groups/bob',
+ 'commonName' => 'bob',
+ 'status' => 1,
+ 'readOnly' => true,
+ '{http://owncloud.org/ns}principal' => $principal,
+ '{http://owncloud.org/ns}group-share' => true,
+ ]
+ ];
+
+ $this->shareCache->expects(self::once())
+ ->method('get')
+ ->with((string)$resourceId)
+ ->willReturn(null);
+ $service->expects(self::once())
+ ->method('getShares')
+ ->with($resourceId)
+ ->willReturn($rows);
+ $this->principalBackend->expects(self::once())
+ ->method('getPrincipalByPath')
+ ->with($principal)
+ ->willReturn(['uri' => $principal, '{DAV:}displayname' => 'bob']);
+ $this->shareCache->expects(self::once())
+ ->method('set')
+ ->with((string)$resourceId, $expected);
+
+ $result = $backend->getShares($resourceId);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testPreloadShares(): void {
+ $resourceIds = [42, 99];
+ $rows = [
+ [
+ 'resourceid' => 42,
+ 'principaluri' => 'principals/groups/bob',
+ 'access' => Backend::ACCESS_READ,
+ ],
+ [
+ 'resourceid' => 99,
+ 'principaluri' => 'principals/users/carlos',
+ 'access' => Backend::ACCESS_READ_WRITE,
+ ]
+ ];
+ $principalResults = [
+ ['uri' => 'principals/groups/bob', '{DAV:}displayname' => 'bob'],
+ ['uri' => 'principals/users/carlos', '{DAV:}displayname' => 'carlos'],
+ ];
+
+ $this->shareCache->expects(self::exactly(2))
+ ->method('get')
+ ->willReturn(null);
+ $this->calendarService->expects(self::once())
+ ->method('getSharesForIds')
+ ->with($resourceIds)
+ ->willReturn($rows);
+ $this->principalBackend->expects(self::exactly(2))
+ ->method('getPrincipalByPath')
+ ->willReturnCallback(function (string $principal) use ($principalResults) {
+ switch ($principal) {
+ case 'principals/groups/bob':
+ return $principalResults[0];
+ default:
+ return $principalResults[1];
+ }
+ });
+ $this->shareCache->expects(self::exactly(2))
+ ->method('set');
+
+ $this->backend->preloadShares($resourceIds);
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/Sharing/PluginTest.php b/apps/dav/tests/unit/DAV/Sharing/PluginTest.php
new file mode 100644
index 00000000000..7a88f7cc5dd
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/Sharing/PluginTest.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\DAV\Sharing;
+
+use OCA\DAV\Connector\Sabre\Auth;
+use OCA\DAV\DAV\Sharing\IShareable;
+use OCA\DAV\DAV\Sharing\Plugin;
+use OCP\IConfig;
+use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Server;
+use Sabre\DAV\SimpleCollection;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+use Test\TestCase;
+
+class PluginTest extends TestCase {
+ private Plugin $plugin;
+ private Server $server;
+ private IShareable&MockObject $book;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $authBackend = $this->createMock(Auth::class);
+ $authBackend->method('isDavAuthenticated')->willReturn(true);
+
+ $request = $this->createMock(IRequest::class);
+ $config = $this->createMock(IConfig::class);
+ $this->plugin = new Plugin($authBackend, $request, $config);
+
+ $root = new SimpleCollection('root');
+ $this->server = new \Sabre\DAV\Server($root);
+ /** @var SimpleCollection $node */
+ $this->book = $this->createMock(IShareable::class);
+ $this->book->method('getName')->willReturn('addressbook1.vcf');
+ $root->addChild($this->book);
+ $this->plugin->initialize($this->server);
+ }
+
+ public function testSharing(): void {
+ $this->book->expects($this->once())->method('updateShares')->with([[
+ 'href' => 'principal:principals/admin',
+ 'commonName' => null,
+ 'summary' => null,
+ 'readOnly' => false
+ ]], ['mailto:wilfredo@example.com']);
+
+ // setup request
+ $request = new Request('POST', 'addressbook1.vcf');
+ $request->addHeader('Content-Type', 'application/xml');
+ $request->setBody('<?xml version="1.0" encoding="utf-8" ?><CS:share xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns"><CS:set><D:href>principal:principals/admin</D:href><CS:read-write/></CS:set> <CS:remove><D:href>mailto:wilfredo@example.com</D:href></CS:remove></CS:share>');
+ $response = new Response();
+ $this->plugin->httpPost($request, $response);
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php b/apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php
new file mode 100644
index 00000000000..3df861accf2
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php
@@ -0,0 +1,100 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\DAV;
+
+use OCA\DAV\DAV\SystemPrincipalBackend;
+use Sabre\DAV\Exception;
+use Test\TestCase;
+
+class SystemPrincipalBackendTest extends TestCase {
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesPrefix')]
+ public function testGetPrincipalsByPrefix(array $expected, string $prefix): void {
+ $backend = new SystemPrincipalBackend();
+ $result = $backend->getPrincipalsByPrefix($prefix);
+ $this->assertEquals($expected, $result);
+ }
+
+ public static function providesPrefix(): array {
+ return [
+ [[], ''],
+ [[[
+ 'uri' => 'principals/system/system',
+ '{DAV:}displayname' => 'system',
+ ],
+ [
+ 'uri' => 'principals/system/public',
+ '{DAV:}displayname' => 'public',
+ ]
+ ], 'principals/system'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesPath')]
+ public function testGetPrincipalByPath(?array $expected, string $path): void {
+ $backend = new SystemPrincipalBackend();
+ $result = $backend->getPrincipalByPath($path);
+ $this->assertEquals($expected, $result);
+ }
+
+ public static function providesPath(): array {
+ return [
+ [null, ''],
+ [null, 'principals'],
+ [null, 'principals/system'],
+ [[
+ 'uri' => 'principals/system/system',
+ '{DAV:}displayname' => 'system',
+ ], 'principals/system/system'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesPrincipalForGetGroupMemberSet')]
+ public function testGetGroupMemberSetExceptional(?string $principal): void {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Principal not found');
+
+ $backend = new SystemPrincipalBackend();
+ $backend->getGroupMemberSet($principal);
+ }
+
+ public static function providesPrincipalForGetGroupMemberSet(): array {
+ return [
+ [null],
+ ['principals/system'],
+ ];
+ }
+
+ public function testGetGroupMemberSet(): void {
+ $backend = new SystemPrincipalBackend();
+ $result = $backend->getGroupMemberSet('principals/system/system');
+ $this->assertEquals(['principals/system/system'], $result);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesPrincipalForGetGroupMembership')]
+ public function testGetGroupMembershipExceptional(string $principal): void {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Principal not found');
+
+ $backend = new SystemPrincipalBackend();
+ $backend->getGroupMembership($principal);
+ }
+
+ public static function providesPrincipalForGetGroupMembership(): array {
+ return [
+ ['principals/system/a'],
+ ];
+ }
+
+ public function testGetGroupMembership(): void {
+ $backend = new SystemPrincipalBackend();
+ $result = $backend->getGroupMembership('principals/system/system');
+ $this->assertEquals([], $result);
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php b/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php
new file mode 100644
index 00000000000..eefbc53fd22
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php
@@ -0,0 +1,167 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2019 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\DAV;
+
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use OCA\DAV\Connector\Sabre\File as DavFile;
+use OCA\DAV\DAV\ViewOnlyPlugin;
+use OCA\Files_Sharing\SharedStorage;
+use OCA\Files_Versions\Sabre\VersionFile;
+use OCA\Files_Versions\Versions\IVersion;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\Storage\ISharedStorage;
+use OCP\Files\Storage\IStorage;
+use OCP\IUser;
+use OCP\Share\IAttributes;
+use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\RequestInterface;
+use Test\TestCase;
+
+class ViewOnlyPluginTest extends TestCase {
+ private Tree&MockObject $tree;
+ private RequestInterface&MockObject $request;
+ private Folder&MockObject $userFolder;
+ private ViewOnlyPlugin $plugin;
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->userFolder = $this->createMock(Folder::class);
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->tree = $this->createMock(Tree::class);
+ $server = $this->createMock(Server::class);
+
+ $this->plugin = new ViewOnlyPlugin(
+ $this->userFolder,
+ );
+ $server->tree = $this->tree;
+
+ $this->plugin->initialize($server);
+ }
+
+ public function testCanGetNonDav(): void {
+ $this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');
+ $this->tree->method('getNodeForPath')->willReturn(null);
+
+ $this->assertTrue($this->plugin->checkViewOnly($this->request));
+ }
+
+ public function testCanGetNonShared(): void {
+ $this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');
+ $davNode = $this->createMock(DavFile::class);
+ $this->tree->method('getNodeForPath')->willReturn($davNode);
+
+ $file = $this->createMock(File::class);
+ $davNode->method('getNode')->willReturn($file);
+
+ $storage = $this->createMock(IStorage::class);
+ $file->method('getStorage')->willReturn($storage);
+ $storage->method('instanceOfStorage')->with(ISharedStorage::class)->willReturn(false);
+
+ $this->assertTrue($this->plugin->checkViewOnly($this->request));
+ }
+
+ public static function providesDataForCanGet(): array {
+ return [
+ // has attribute permissions-download enabled - can get file
+ [false, true, true, true],
+ // has no attribute permissions-download - can get file
+ [false, null, true, true],
+ // has attribute permissions-download enabled - can get file version
+ [true, true, true, true],
+ // has no attribute permissions-download - can get file version
+ [true, null, true, true],
+ // has attribute permissions-download disabled - cannot get the file
+ [false, false, false, false],
+ // has attribute permissions-download disabled - cannot get the file version
+ [true, false, false, false],
+
+ // Has global allowViewWithoutDownload option enabled
+ // has attribute permissions-download disabled - can get file
+ [false, false, false, true],
+ // has attribute permissions-download disabled - can get file version
+ [true, false, false, true],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesDataForCanGet')]
+ public function testCanGet(bool $isVersion, ?bool $attrEnabled, bool $expectCanDownloadFile, bool $allowViewWithoutDownload): void {
+ $nodeInfo = $this->createMock(File::class);
+ if ($isVersion) {
+ $davPath = 'versions/alice/versions/117/123456';
+ $version = $this->createMock(IVersion::class);
+ $version->expects($this->once())
+ ->method('getSourceFile')
+ ->willReturn($nodeInfo);
+ $davNode = $this->createMock(VersionFile::class);
+ $davNode->expects($this->once())
+ ->method('getVersion')
+ ->willReturn($version);
+
+ $currentUser = $this->createMock(IUser::class);
+ $currentUser->expects($this->once())
+ ->method('getUID')
+ ->willReturn('alice');
+ $nodeInfo->expects($this->once())
+ ->method('getOwner')
+ ->willReturn($currentUser);
+
+ $nodeInfo = $this->createMock(File::class);
+ $owner = $this->createMock(IUser::class);
+ $owner->expects($this->once())
+ ->method('getUID')
+ ->willReturn('bob');
+ $this->userFolder->expects($this->once())
+ ->method('getById')
+ ->willReturn([$nodeInfo]);
+ $this->userFolder->expects($this->once())
+ ->method('getOwner')
+ ->willReturn($owner);
+ } else {
+ $davPath = 'files/path/to/file.odt';
+ $davNode = $this->createMock(DavFile::class);
+ $davNode->method('getNode')->willReturn($nodeInfo);
+ }
+
+ $this->request->expects($this->once())->method('getPath')->willReturn($davPath);
+
+ $this->tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with($davPath)
+ ->willReturn($davNode);
+
+ $storage = $this->createMock(SharedStorage::class);
+ $share = $this->createMock(IShare::class);
+ $nodeInfo->expects($this->once())
+ ->method('getStorage')
+ ->willReturn($storage);
+ $storage->method('instanceOfStorage')->with(ISharedStorage::class)->willReturn(true);
+ $storage->method('getShare')->willReturn($share);
+
+ $extAttr = $this->createMock(IAttributes::class);
+ $share->method('getAttributes')->willReturn($extAttr);
+ $extAttr->expects($this->once())
+ ->method('getAttribute')
+ ->with('permissions', 'download')
+ ->willReturn($attrEnabled);
+
+ $share->expects($this->once())
+ ->method('canSeeContent')
+ ->willReturn($allowViewWithoutDownload);
+
+ if (!$expectCanDownloadFile) {
+ $this->expectException(Forbidden::class);
+ }
+ $this->plugin->checkViewOnly($this->request);
+ }
+}
diff --git a/apps/dav/tests/unit/Direct/DirectFileTest.php b/apps/dav/tests/unit/Direct/DirectFileTest.php
new file mode 100644
index 00000000000..f6f0f49fa8c
--- /dev/null
+++ b/apps/dav/tests/unit/Direct/DirectFileTest.php
@@ -0,0 +1,111 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Direct;
+
+use OCA\DAV\Db\Direct;
+use OCA\DAV\Direct\DirectFile;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\Forbidden;
+use Test\TestCase;
+
+class DirectFileTest extends TestCase {
+ private Direct $direct;
+ private IRootFolder&MockObject $rootFolder;
+ private Folder&MockObject $userFolder;
+ private File&MockObject $file;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private DirectFile $directFile;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->direct = Direct::fromParams([
+ 'userId' => 'directUser',
+ 'token' => 'directToken',
+ 'fileId' => 42,
+ ]);
+
+ $this->rootFolder = $this->createMock(IRootFolder::class);
+
+ $this->userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with('directUser')
+ ->willReturn($this->userFolder);
+
+ $this->file = $this->createMock(File::class);
+ $this->userFolder->method('getFirstNodeById')
+ ->with(42)
+ ->willReturn($this->file);
+
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+
+ $this->directFile = new DirectFile($this->direct, $this->rootFolder, $this->eventDispatcher);
+ }
+
+ public function testPut(): void {
+ $this->expectException(Forbidden::class);
+
+ $this->directFile->put('foo');
+ }
+
+ public function testGet(): void {
+ $this->file->expects($this->once())
+ ->method('fopen')
+ ->with('rb');
+ $this->directFile->get();
+ }
+
+ public function testGetContentType(): void {
+ $this->file->method('getMimeType')
+ ->willReturn('direct/type');
+
+ $this->assertSame('direct/type', $this->directFile->getContentType());
+ }
+
+ public function testGetETag(): void {
+ $this->file->method('getEtag')
+ ->willReturn('directEtag');
+
+ $this->assertSame('directEtag', $this->directFile->getETag());
+ }
+
+ public function testGetSize(): void {
+ $this->file->method('getSize')
+ ->willReturn(42);
+
+ $this->assertSame(42, $this->directFile->getSize());
+ }
+
+ public function testDelete(): void {
+ $this->expectException(Forbidden::class);
+
+ $this->directFile->delete();
+ }
+
+ public function testGetName(): void {
+ $this->assertSame('directToken', $this->directFile->getName());
+ }
+
+ public function testSetName(): void {
+ $this->expectException(Forbidden::class);
+
+ $this->directFile->setName('foobar');
+ }
+
+ public function testGetLastModified(): void {
+ $this->file->method('getMTime')
+ ->willReturn(42);
+
+ $this->assertSame(42, $this->directFile->getLastModified());
+ }
+}
diff --git a/apps/dav/tests/unit/Direct/DirectHomeTest.php b/apps/dav/tests/unit/Direct/DirectHomeTest.php
new file mode 100644
index 00000000000..94c82c2b7c5
--- /dev/null
+++ b/apps/dav/tests/unit/Direct/DirectHomeTest.php
@@ -0,0 +1,160 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Direct;
+
+use OCA\DAV\Db\Direct;
+use OCA\DAV\Db\DirectMapper;
+use OCA\DAV\Direct\DirectFile;
+use OCA\DAV\Direct\DirectHome;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IRootFolder;
+use OCP\IRequest;
+use OCP\Security\Bruteforce\IThrottler;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\MethodNotAllowed;
+use Sabre\DAV\Exception\NotFound;
+use Test\TestCase;
+
+class DirectHomeTest extends TestCase {
+ private DirectMapper&MockObject $directMapper;
+ private IRootFolder&MockObject $rootFolder;
+ private ITimeFactory&MockObject $timeFactory;
+ private IThrottler&MockObject $throttler;
+ private IRequest&MockObject $request;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private DirectHome $directHome;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->directMapper = $this->createMock(DirectMapper::class);
+ $this->rootFolder = $this->createMock(IRootFolder::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->throttler = $this->createMock(IThrottler::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+
+ $this->timeFactory->method('getTime')
+ ->willReturn(42);
+
+ $this->request->method('getRemoteAddress')
+ ->willReturn('1.2.3.4');
+
+
+ $this->directHome = new DirectHome(
+ $this->rootFolder,
+ $this->directMapper,
+ $this->timeFactory,
+ $this->throttler,
+ $this->request,
+ $this->eventDispatcher
+ );
+ }
+
+ public function testCreateFile(): void {
+ $this->expectException(Forbidden::class);
+
+ $this->directHome->createFile('foo', 'bar');
+ }
+
+ public function testCreateDirectory(): void {
+ $this->expectException(Forbidden::class);
+
+ $this->directHome->createDirectory('foo');
+ }
+
+ public function testGetChildren(): void {
+ $this->expectException(MethodNotAllowed::class);
+
+ $this->directHome->getChildren();
+ }
+
+ public function testChildExists(): void {
+ $this->assertFalse($this->directHome->childExists('foo'));
+ }
+
+ public function testDelete(): void {
+ $this->expectException(Forbidden::class);
+
+ $this->directHome->delete();
+ }
+
+ public function testGetName(): void {
+ $this->assertSame('direct', $this->directHome->getName());
+ }
+
+ public function testSetName(): void {
+ $this->expectException(Forbidden::class);
+
+ $this->directHome->setName('foo');
+ }
+
+ public function testGetLastModified(): void {
+ $this->assertSame(0, $this->directHome->getLastModified());
+ }
+
+ public function testGetChildValid(): void {
+ $direct = Direct::fromParams([
+ 'expiration' => 100,
+ ]);
+
+ $this->directMapper->method('getByToken')
+ ->with('longtoken')
+ ->willReturn($direct);
+
+ $this->throttler->expects($this->never())
+ ->method($this->anything());
+
+ $result = $this->directHome->getChild('longtoken');
+ $this->assertInstanceOf(DirectFile::class, $result);
+ }
+
+ public function testGetChildExpired(): void {
+ $direct = Direct::fromParams([
+ 'expiration' => 41,
+ ]);
+
+ $this->directMapper->method('getByToken')
+ ->with('longtoken')
+ ->willReturn($direct);
+
+ $this->throttler->expects($this->never())
+ ->method($this->anything());
+
+ $this->expectException(NotFound::class);
+
+ $this->directHome->getChild('longtoken');
+ }
+
+ public function testGetChildInvalid(): void {
+ $this->directMapper->method('getByToken')
+ ->with('longtoken')
+ ->willThrowException(new DoesNotExistException('not found'));
+
+ $this->throttler->expects($this->once())
+ ->method('registerAttempt')
+ ->with(
+ 'directlink',
+ '1.2.3.4'
+ );
+ $this->throttler->expects($this->once())
+ ->method('sleepDelayOrThrowOnMax')
+ ->with(
+ '1.2.3.4',
+ 'directlink'
+ );
+
+ $this->expectException(NotFound::class);
+
+ $this->directHome->getChild('longtoken');
+ }
+}
diff --git a/apps/dav/tests/unit/Files/FileSearchBackendTest.php b/apps/dav/tests/unit/Files/FileSearchBackendTest.php
new file mode 100644
index 00000000000..c6d6f85347b
--- /dev/null
+++ b/apps/dav/tests/unit/Files/FileSearchBackendTest.php
@@ -0,0 +1,421 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Files;
+
+use OC\Files\Search\SearchComparison;
+use OC\Files\Search\SearchQuery;
+use OC\Files\View;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\File;
+use OCA\DAV\Connector\Sabre\FilesPlugin;
+use OCA\DAV\Connector\Sabre\ObjectTree;
+use OCA\DAV\Connector\Sabre\Server;
+use OCA\DAV\Files\FileSearchBackend;
+use OCP\Files\FileInfo;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\Search\ISearchBinaryOperator;
+use OCP\Files\Search\ISearchComparison;
+use OCP\Files\Search\ISearchQuery;
+use OCP\FilesMetadata\IFilesMetadataManager;
+use OCP\IUser;
+use OCP\Share\IManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use SearchDAV\Backend\SearchPropertyDefinition;
+use SearchDAV\Query\Limit;
+use SearchDAV\Query\Literal;
+use SearchDAV\Query\Operator;
+use SearchDAV\Query\Query;
+use SearchDAV\Query\Scope;
+use Test\TestCase;
+
+class FileSearchBackendTest extends TestCase {
+ private ObjectTree&MockObject $tree;
+ private Server&MockObject $server;
+ private IUser&MockObject $user;
+ private IRootFolder&MockObject $rootFolder;
+ private IManager&MockObject $shareManager;
+ private View&MockObject $view;
+ private Folder&MockObject $searchFolder;
+ private Directory&MockObject $davFolder;
+ private FileSearchBackend $search;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->user = $this->createMock(IUser::class);
+ $this->user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('test');
+
+ $this->tree = $this->createMock(ObjectTree::class);
+ $this->server = $this->createMock(Server::class);
+ $this->view = $this->createMock(View::class);
+ $this->rootFolder = $this->createMock(IRootFolder::class);
+ $this->shareManager = $this->createMock(IManager::class);
+ $this->searchFolder = $this->createMock(Folder::class);
+ $fileInfo = $this->createMock(FileInfo::class);
+ $this->davFolder = $this->createMock(Directory::class);
+
+ $this->view->expects($this->any())
+ ->method('getRoot')
+ ->willReturn('');
+
+ $this->view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturnArgument(0);
+
+ $this->davFolder->expects($this->any())
+ ->method('getFileInfo')
+ ->willReturn($fileInfo);
+
+ $this->rootFolder->expects($this->any())
+ ->method('get')
+ ->willReturn($this->searchFolder);
+
+ $filesMetadataManager = $this->createMock(IFilesMetadataManager::class);
+
+ $this->search = new FileSearchBackend($this->server, $this->tree, $this->user, $this->rootFolder, $this->shareManager, $this->view, $filesMetadataManager);
+ }
+
+ public function testSearchFilename(): void {
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($this->davFolder);
+
+ $this->searchFolder->expects($this->once())
+ ->method('search')
+ ->with(new SearchQuery(
+ new SearchComparison(
+ ISearchComparison::COMPARE_EQUAL,
+ 'name',
+ 'foo'
+ ),
+ 0,
+ 0,
+ [],
+ $this->user
+ ))
+ ->willReturn([
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
+ ]);
+
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
+ $result = $this->search->search($query);
+
+ $this->assertCount(1, $result);
+ $this->assertEquals('/files/test/test/path', $result[0]->href);
+ }
+
+ public function testSearchMimetype(): void {
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($this->davFolder);
+
+ $this->searchFolder->expects($this->once())
+ ->method('search')
+ ->with(new SearchQuery(
+ new SearchComparison(
+ ISearchComparison::COMPARE_EQUAL,
+ 'mimetype',
+ 'foo'
+ ),
+ 0,
+ 0,
+ [],
+ $this->user
+ ))
+ ->willReturn([
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
+ ]);
+
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getcontenttype', 'foo');
+ $result = $this->search->search($query);
+
+ $this->assertCount(1, $result);
+ $this->assertEquals('/files/test/test/path', $result[0]->href);
+ }
+
+ public function testSearchSize(): void {
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($this->davFolder);
+
+ $this->searchFolder->expects($this->once())
+ ->method('search')
+ ->with(new SearchQuery(
+ new SearchComparison(
+ ISearchComparison::COMPARE_GREATER_THAN,
+ 'size',
+ 10
+ ),
+ 0,
+ 0,
+ [],
+ $this->user
+ ))
+ ->willReturn([
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
+ ]);
+
+ $query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, FilesPlugin::SIZE_PROPERTYNAME, 10);
+ $result = $this->search->search($query);
+
+ $this->assertCount(1, $result);
+ $this->assertEquals('/files/test/test/path', $result[0]->href);
+ }
+
+ public function testSearchMtime(): void {
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($this->davFolder);
+
+ $this->searchFolder->expects($this->once())
+ ->method('search')
+ ->with(new SearchQuery(
+ new SearchComparison(
+ ISearchComparison::COMPARE_GREATER_THAN,
+ 'mtime',
+ 10
+ ),
+ 0,
+ 0,
+ [],
+ $this->user
+ ))
+ ->willReturn([
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
+ ]);
+
+ $query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, '{DAV:}getlastmodified', 10);
+ $result = $this->search->search($query);
+
+ $this->assertCount(1, $result);
+ $this->assertEquals('/files/test/test/path', $result[0]->href);
+ }
+
+ public function testSearchIsCollection(): void {
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($this->davFolder);
+
+ $this->searchFolder->expects($this->once())
+ ->method('search')
+ ->with(new SearchQuery(
+ new SearchComparison(
+ ISearchComparison::COMPARE_EQUAL,
+ 'mimetype',
+ FileInfo::MIMETYPE_FOLDER
+ ),
+ 0,
+ 0,
+ [],
+ $this->user
+ ))
+ ->willReturn([
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
+ ]);
+
+ $query = $this->getBasicQuery(Operator::OPERATION_IS_COLLECTION, 'yes');
+ $result = $this->search->search($query);
+
+ $this->assertCount(1, $result);
+ $this->assertEquals('/files/test/test/path', $result[0]->href);
+ }
+
+
+ public function testSearchInvalidProp(): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($this->davFolder);
+
+ $this->searchFolder->expects($this->never())
+ ->method('search');
+
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getetag', 'foo');
+ $this->search->search($query);
+ }
+
+ private function getBasicQuery(string $type, string $property, int|string|null $value = null) {
+ $scope = new Scope('/', 'infinite');
+ $scope->path = '/';
+ $from = [$scope];
+ $orderBy = [];
+ $select = [];
+ if (is_null($value)) {
+ $where = new Operator(
+ $type,
+ [new Literal($property)]
+ );
+ } else {
+ $where = new Operator(
+ $type,
+ [new SearchPropertyDefinition($property, true, true, true), new Literal($value)]
+ );
+ }
+ $limit = new Limit();
+
+ return new Query($select, $from, $where, $orderBy, $limit);
+ }
+
+
+ public function testSearchNonFolder(): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $davNode = $this->createMock(File::class);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($davNode);
+
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
+ $this->search->search($query);
+ }
+
+ public function testSearchLimitOwnerBasic(): void {
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($this->davFolder);
+
+ /** @var ISearchQuery|null $receivedQuery */
+ $receivedQuery = null;
+ $this->searchFolder
+ ->method('search')
+ ->willReturnCallback(function ($query) use (&$receivedQuery) {
+ $receivedQuery = $query;
+ return [
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
+ ];
+ });
+
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
+ $this->search->search($query);
+
+ $this->assertNotNull($receivedQuery);
+ $this->assertTrue($receivedQuery->limitToHome());
+
+ /** @var ISearchBinaryOperator $operator */
+ $operator = $receivedQuery->getSearchOperation();
+ $this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
+ $this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
+ $this->assertEmpty($operator->getArguments());
+ }
+
+ public function testSearchLimitOwnerNested(): void {
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($this->davFolder);
+
+ /** @var ISearchQuery|null $receivedQuery */
+ $receivedQuery = null;
+ $this->searchFolder
+ ->method('search')
+ ->willReturnCallback(function ($query) use (&$receivedQuery) {
+ $receivedQuery = $query;
+ return [
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
+ ];
+ });
+
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
+ $query->where = new Operator(
+ Operator::OPERATION_AND,
+ [
+ new Operator(
+ Operator::OPERATION_EQUAL,
+ [new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new Literal('image/png')]
+ ),
+ new Operator(
+ Operator::OPERATION_EQUAL,
+ [new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, true), new Literal($this->user->getUID())]
+ ),
+ ]
+ );
+ $this->search->search($query);
+
+ $this->assertNotNull($receivedQuery);
+ $this->assertTrue($receivedQuery->limitToHome());
+
+ /** @var ISearchBinaryOperator $operator */
+ $operator = $receivedQuery->getSearchOperation();
+ $this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
+ $this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
+ $this->assertCount(2, $operator->getArguments());
+
+ /** @var ISearchBinaryOperator $operator */
+ $operator = $operator->getArguments()[1];
+ $this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
+ $this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
+ $this->assertEmpty($operator->getArguments());
+ }
+
+ public function testSearchOperatorLimit(): void {
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($this->davFolder);
+
+ $innerOperator = new Operator(
+ Operator::OPERATION_EQUAL,
+ [new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new Literal('image/png')]
+ );
+ // 5 child operators
+ $level1Operator = new Operator(
+ Operator::OPERATION_AND,
+ [
+ $innerOperator,
+ $innerOperator,
+ $innerOperator,
+ $innerOperator,
+ $innerOperator,
+ ]
+ );
+ // 5^2 = 25 child operators
+ $level2Operator = new Operator(
+ Operator::OPERATION_AND,
+ [
+ $level1Operator,
+ $level1Operator,
+ $level1Operator,
+ $level1Operator,
+ $level1Operator,
+ ]
+ );
+ // 5^3 = 125 child operators
+ $level3Operator = new Operator(
+ Operator::OPERATION_AND,
+ [
+ $level2Operator,
+ $level2Operator,
+ $level2Operator,
+ $level2Operator,
+ $level2Operator,
+ ]
+ );
+
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
+ $query->where = $level3Operator;
+ $this->expectException(\InvalidArgumentException::class);
+ $this->search->search($query);
+ }
+
+ public function testPreloadPropertyFor(): void {
+ $node1 = $this->createMock(File::class);
+ $node2 = $this->createMock(Directory::class);
+ $nodes = [$node1, $node2];
+ $requestProperties = ['{DAV:}getcontenttype', '{DAV:}getlastmodified'];
+
+ $this->server->expects($this->once())
+ ->method('emit')
+ ->with('preloadProperties', [$nodes, $requestProperties]);
+
+ $this->search->preloadPropertyFor($nodes, $requestProperties);
+ }
+}
diff --git a/apps/dav/tests/unit/Files/MultipartRequestParserTest.php b/apps/dav/tests/unit/Files/MultipartRequestParserTest.php
new file mode 100644
index 00000000000..dc0e884f07c
--- /dev/null
+++ b/apps/dav/tests/unit/Files/MultipartRequestParserTest.php
@@ -0,0 +1,322 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Tests\unit\Files;
+
+use OCA\DAV\BulkUpload\MultipartRequestParser;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\HTTP\RequestInterface;
+use Test\TestCase;
+
+class MultipartRequestParserTest extends TestCase {
+
+ protected LoggerInterface&MockObject $logger;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->logger = $this->createMock(LoggerInterface::class);
+ }
+
+ private static function getValidBodyObject(): array {
+ return [
+ [
+ 'headers' => [
+ 'Content-Length' => 7,
+ 'X-File-MD5' => '4f2377b4d911f7ec46325fe603c3af03',
+ 'OC-Checksum' => 'md5:4f2377b4d911f7ec46325fe603c3af03',
+ 'X-File-Path' => '/coucou.txt'
+ ],
+ 'content' => "Coucou\n"
+ ]
+ ];
+ }
+
+ private function getMultipartParser(array $parts, array $headers = [], string $boundary = 'boundary_azertyuiop'): MultipartRequestParser {
+ /** @var RequestInterface&MockObject $request */
+ $request = $this->getMockBuilder(RequestInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $headers = array_merge(['Content-Type' => 'multipart/related; boundary=' . $boundary], $headers);
+ $request->expects($this->any())
+ ->method('getHeader')
+ ->willReturnCallback(function (string $key) use (&$headers) {
+ return $headers[$key];
+ });
+
+ $body = '';
+ foreach ($parts as $part) {
+ $body .= '--' . $boundary . "\r\n";
+
+ foreach ($part['headers'] as $headerKey => $headerPart) {
+ $body .= $headerKey . ': ' . $headerPart . "\r\n";
+ }
+
+ $body .= "\r\n";
+ $body .= $part['content'] . "\r\n";
+ }
+
+ $body .= '--' . $boundary . '--';
+
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, $body);
+ rewind($stream);
+
+ $request->expects($this->any())
+ ->method('getBody')
+ ->willReturn($stream);
+
+ return new MultipartRequestParser($request, $this->logger);
+ }
+
+
+ /**
+ * Test validation of the request's body type
+ */
+ public function testBodyTypeValidation(): void {
+ $bodyStream = 'I am not a stream, but pretend to be';
+ /** @var RequestInterface&MockObject $request */
+ $request = $this->getMockBuilder(RequestInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $request->expects($this->any())
+ ->method('getBody')
+ ->willReturn($bodyStream);
+
+ $this->expectExceptionMessage('Body should be of type resource');
+ new MultipartRequestParser($request, $this->logger);
+ }
+
+ /**
+ * Test with valid request.
+ * - valid boundary
+ * - valid hash
+ * - valid content-length
+ * - valid file content
+ * - valid file path
+ */
+ public function testValidRequest(): void {
+ $bodyObject = self::getValidBodyObject();
+ unset($bodyObject['0']['headers']['X-File-MD5']);
+
+ $multipartParser = $this->getMultipartParser($bodyObject);
+
+ [$headers, $content] = $multipartParser->parseNextPart();
+
+ $this->assertSame((int)$headers['content-length'], 7, 'Content-Length header should be the same as provided.');
+ $this->assertSame($headers['oc-checksum'], 'md5:4f2377b4d911f7ec46325fe603c3af03', 'OC-Checksum header should be the same as provided.');
+ $this->assertSame($headers['x-file-path'], '/coucou.txt', 'X-File-Path header should be the same as provided.');
+
+ $this->assertSame($content, "Coucou\n", 'Content should be the same');
+ }
+
+ /**
+ * Test with valid request.
+ * - valid boundary
+ * - valid md5 hash
+ * - valid content-length
+ * - valid file content
+ * - valid file path
+ */
+ public function testValidRequestWithMd5(): void {
+ $bodyObject = self::getValidBodyObject();
+ unset($bodyObject['0']['headers']['OC-Checksum']);
+
+ $multipartParser = $this->getMultipartParser($bodyObject);
+
+ [$headers, $content] = $multipartParser->parseNextPart();
+
+ $this->assertSame((int)$headers['content-length'], 7, 'Content-Length header should be the same as provided.');
+ $this->assertSame($headers['x-file-md5'], '4f2377b4d911f7ec46325fe603c3af03', 'X-File-MD5 header should be the same as provided.');
+ $this->assertSame($headers['x-file-path'], '/coucou.txt', 'X-File-Path header should be the same as provided.');
+
+ $this->assertSame($content, "Coucou\n", 'Content should be the same');
+ }
+
+ /**
+ * Test with invalid hash.
+ */
+ public function testInvalidHash(): void {
+ $bodyObject = self::getValidBodyObject();
+ $bodyObject['0']['headers']['OC-Checksum'] = 'md5:f2377b4d911f7ec46325fe603c3af03';
+ unset($bodyObject['0']['headers']['X-File-MD5']);
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('Computed md5 hash is incorrect (4f2377b4d911f7ec46325fe603c3af03).');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with invalid md5 hash.
+ */
+ public function testInvalidMd5Hash(): void {
+ $bodyObject = self::getValidBodyObject();
+ unset($bodyObject['0']['headers']['OC-Checksum']);
+ $bodyObject['0']['headers']['X-File-MD5'] = 'f2377b4d911f7ec46325fe603c3af03';
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('Computed md5 hash is incorrect (4f2377b4d911f7ec46325fe603c3af03).');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with a null hash headers.
+ */
+ public function testNullHash(): void {
+ $bodyObject = self::getValidBodyObject();
+ unset($bodyObject['0']['headers']['OC-Checksum']);
+ unset($bodyObject['0']['headers']['X-File-MD5']);
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('The hash headers must not be null.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with a null Content-Length.
+ */
+ public function testNullContentLength(): void {
+ $bodyObject = self::getValidBodyObject();
+ unset($bodyObject['0']['headers']['Content-Length']);
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('The Content-Length header must not be null.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with a lower Content-Length.
+ */
+ public function testLowerContentLength(): void {
+ $bodyObject = self::getValidBodyObject();
+ $bodyObject['0']['headers']['Content-Length'] = 6;
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('Computed md5 hash is incorrect (41060d3ddfdf63e68fc2bf196f652ee9).');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with a higher Content-Length.
+ */
+ public function testHigherContentLength(): void {
+ $bodyObject = self::getValidBodyObject();
+ $bodyObject['0']['headers']['Content-Length'] = 8;
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('Computed md5 hash is incorrect (0161002bbee6a744f18741b8a914e413).');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with wrong boundary in body.
+ */
+ public function testWrongBoundary(): void {
+ $bodyObject = self::getValidBodyObject();
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/related; boundary=boundary_poiuytreza']
+ );
+
+ $this->expectExceptionMessage('Boundary not found where it should be.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with no boundary in request headers.
+ */
+ public function testNoBoundaryInHeader(): void {
+ $bodyObject = self::getValidBodyObject();
+ $this->expectExceptionMessage('Error while parsing boundary in Content-Type header.');
+ $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/related']
+ );
+ }
+
+ /**
+ * Test with no boundary in the request's headers.
+ */
+ public function testNoBoundaryInBody(): void {
+ $bodyObject = self::getValidBodyObject();
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/related; boundary=boundary_azertyuiop'],
+ ''
+ );
+
+ $this->expectExceptionMessage('Boundary not found where it should be.');
+ $multipartParser->parseNextPart();
+ }
+
+ /**
+ * Test with a boundary with quotes in the request's headers.
+ */
+ public function testBoundaryWithQuotes(): void {
+ $bodyObject = self::getValidBodyObject();
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/related; boundary="boundary_azertyuiop"'],
+ );
+
+ $multipartParser->parseNextPart();
+
+ // Dummy assertion, we just want to test that the parsing works.
+ $this->assertTrue(true);
+ }
+
+ /**
+ * Test with a wrong Content-Type in the request's headers.
+ */
+ public function testWrongContentType(): void {
+ $bodyObject = self::getValidBodyObject();
+ $this->expectExceptionMessage('Content-Type must be multipart/related');
+ $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/form-data; boundary="boundary_azertyuiop"'],
+ );
+ }
+
+ /**
+ * Test with a wrong key after the content type in the request's headers.
+ */
+ public function testWrongKeyInContentType(): void {
+ $bodyObject = self::getValidBodyObject();
+ $this->expectExceptionMessage('Boundary is invalid');
+ $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => 'multipart/related; wrongkey="boundary_azertyuiop"'],
+ );
+ }
+
+ /**
+ * Test with a null Content-Type in the request's headers.
+ */
+ public function testNullContentType(): void {
+ $bodyObject = self::getValidBodyObject();
+ $this->expectExceptionMessage('Content-Type can not be null');
+ $this->getMultipartParser(
+ $bodyObject,
+ ['Content-Type' => null],
+
+ );
+ }
+}
diff --git a/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php b/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php
new file mode 100644
index 00000000000..1a7ab7179e1
--- /dev/null
+++ b/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php
@@ -0,0 +1,258 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Files\Sharing;
+
+use OCA\DAV\Files\Sharing\FilesDropPlugin;
+use OCP\Files\Folder;
+use OCP\Files\NotFoundException;
+use OCP\Share\IAttributes;
+use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\Server;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+class FilesDropPluginTest extends TestCase {
+
+ private FilesDropPlugin $plugin;
+
+ private Folder&MockObject $node;
+ private IShare&MockObject $share;
+ private Server&MockObject $server;
+ private RequestInterface&MockObject $request;
+ private ResponseInterface&MockObject $response;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->node = $this->createMock(Folder::class);
+ $this->node->method('getPath')
+ ->willReturn('/files/token');
+
+ $this->share = $this->createMock(IShare::class);
+ $this->share->expects(self::any())
+ ->method('getNode')
+ ->willReturn($this->node);
+ $this->server = $this->createMock(Server::class);
+ $this->plugin = new FilesDropPlugin();
+
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->response = $this->createMock(ResponseInterface::class);
+
+ $attributes = $this->createMock(IAttributes::class);
+ $this->share->expects($this->any())
+ ->method('getAttributes')
+ ->willReturn($attributes);
+
+ $this->share
+ ->method('getToken')
+ ->willReturn('token');
+ }
+
+ public function testNotEnabled(): void {
+ $this->request->expects($this->never())
+ ->method($this->anything());
+
+ $this->plugin->beforeMethod($this->request, $this->response);
+ }
+
+ public function testValid(): void {
+ $this->plugin->enable();
+ $this->plugin->setShare($this->share);
+
+ $this->request->method('getMethod')
+ ->willReturn('PUT');
+
+ $this->request->method('getPath')
+ ->willReturn('/files/token/file.txt');
+
+ $this->request->method('getBaseUrl')
+ ->willReturn('https://example.com');
+
+ $this->node->expects(self::once())
+ ->method('getNonExistingName')
+ ->with('file.txt')
+ ->willReturn('file.txt');
+
+ $this->request->expects($this->once())
+ ->method('setUrl')
+ ->with('https://example.com/files/token/file.txt');
+
+ $this->plugin->beforeMethod($this->request, $this->response);
+ }
+
+ public function testFileAlreadyExistsValid(): void {
+ $this->plugin->enable();
+ $this->plugin->setShare($this->share);
+
+ $this->request->method('getMethod')
+ ->willReturn('PUT');
+
+ $this->request->method('getPath')
+ ->willReturn('/files/token/file.txt');
+
+ $this->request->method('getBaseUrl')
+ ->willReturn('https://example.com');
+
+ $this->node->method('getNonExistingName')
+ ->with('file.txt')
+ ->willReturn('file (2).txt');
+
+ $this->request->expects($this->once())
+ ->method('setUrl')
+ ->with($this->equalTo('https://example.com/files/token/file (2).txt'));
+
+ $this->plugin->beforeMethod($this->request, $this->response);
+ }
+
+ public function testNoMKCOLWithoutNickname(): void {
+ $this->plugin->enable();
+ $this->plugin->setShare($this->share);
+
+ $this->request->method('getMethod')
+ ->willReturn('MKCOL');
+
+ $this->expectException(BadRequest::class);
+
+ $this->plugin->beforeMethod($this->request, $this->response);
+ }
+
+ public function testMKCOLWithNickname(): void {
+ $this->plugin->enable();
+ $this->plugin->setShare($this->share);
+
+ $this->request->method('getMethod')
+ ->willReturn('MKCOL');
+
+ $this->request->method('hasHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn(true);
+ $this->request->method('getHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn('nickname');
+
+ $this->expectNotToPerformAssertions();
+
+ $this->plugin->beforeMethod($this->request, $this->response);
+ }
+
+ public function testSubdirPut(): void {
+ $this->plugin->enable();
+ $this->plugin->setShare($this->share);
+
+ $this->request->method('getMethod')
+ ->willReturn('PUT');
+
+ $this->request->method('hasHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn(true);
+ $this->request->method('getHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn('nickname');
+
+ $this->request->method('getPath')
+ ->willReturn('/files/token/folder/file.txt');
+
+ $this->request->method('getBaseUrl')
+ ->willReturn('https://example.com');
+
+ $nodeName = $this->createMock(Folder::class);
+ $nodeFolder = $this->createMock(Folder::class);
+ $nodeFolder->expects(self::once())
+ ->method('getPath')
+ ->willReturn('/files/token/nickname/folder');
+ $nodeFolder->method('getNonExistingName')
+ ->with('file.txt')
+ ->willReturn('file.txt');
+ $nodeName->expects(self::once())
+ ->method('get')
+ ->with('folder')
+ ->willThrowException(new NotFoundException());
+ $nodeName->expects(self::once())
+ ->method('newFolder')
+ ->with('folder')
+ ->willReturn($nodeFolder);
+
+ $this->node->expects(self::once())
+ ->method('get')
+ ->willThrowException(new NotFoundException());
+ $this->node->expects(self::once())
+ ->method('newFolder')
+ ->with('nickname')
+ ->willReturn($nodeName);
+
+ $this->request->expects($this->once())
+ ->method('setUrl')
+ ->with($this->equalTo('https://example.com/files/token/nickname/folder/file.txt'));
+
+ $this->plugin->beforeMethod($this->request, $this->response);
+ }
+
+ public function testRecursiveFolderCreation(): void {
+ $this->plugin->enable();
+ $this->plugin->setShare($this->share);
+
+ $this->request->method('getMethod')
+ ->willReturn('PUT');
+ $this->request->method('hasHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn(true);
+ $this->request->method('getHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn('nickname');
+
+ $this->request->method('getPath')
+ ->willReturn('/files/token/folder/subfolder/file.txt');
+ $this->request->method('getBaseUrl')
+ ->willReturn('https://example.com');
+
+ $this->request->expects($this->once())
+ ->method('setUrl')
+ ->with($this->equalTo('https://example.com/files/token/nickname/folder/subfolder/file.txt'));
+
+ $subfolder = $this->createMock(Folder::class);
+ $subfolder->expects(self::once())
+ ->method('getNonExistingName')
+ ->with('file.txt')
+ ->willReturn('file.txt');
+ $subfolder->expects(self::once())
+ ->method('getPath')
+ ->willReturn('/files/token/nickname/folder/subfolder');
+
+ $folder = $this->createMock(Folder::class);
+ $folder->expects(self::once())
+ ->method('get')
+ ->with('subfolder')
+ ->willReturn($subfolder);
+
+ $nickname = $this->createMock(Folder::class);
+ $nickname->expects(self::once())
+ ->method('get')
+ ->with('folder')
+ ->willReturn($folder);
+
+ $this->node->method('get')
+ ->with('nickname')
+ ->willReturn($nickname);
+ $this->plugin->beforeMethod($this->request, $this->response);
+ }
+
+ public function testOnMkcol(): void {
+ $this->plugin->enable();
+ $this->plugin->setShare($this->share);
+
+ $this->response->expects($this->once())
+ ->method('setStatus')
+ ->with(201);
+
+ $response = $this->plugin->onMkcol($this->request, $this->response);
+ $this->assertFalse($response);
+ }
+}
diff --git a/apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php b/apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php
new file mode 100644
index 00000000000..8519dca7126
--- /dev/null
+++ b/apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Listener;
+
+use OCA\DAV\CalDAV\Activity\Backend as ActivityBackend;
+use OCA\DAV\CalDAV\Activity\Provider\Event;
+use OCA\DAV\DAV\Sharing\Plugin as SharingPlugin;
+use OCA\DAV\Events\CalendarDeletedEvent;
+use OCA\DAV\Listener\ActivityUpdaterListener;
+use OCP\Calendar\Events\CalendarObjectDeletedEvent;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class ActivityUpdaterListenerTest extends TestCase {
+
+ private ActivityBackend&MockObject $activityBackend;
+ private LoggerInterface&MockObject $logger;
+ private ActivityUpdaterListener $listener;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->activityBackend = $this->createMock(ActivityBackend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->listener = new ActivityUpdaterListener(
+ $this->activityBackend,
+ $this->logger
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestHandleCalendarObjectDeletedEvent')]
+ public function testHandleCalendarObjectDeletedEvent(int $calendarId, array $calendarData, array $shares, array $objectData, bool $createsActivity): void {
+ $event = new CalendarObjectDeletedEvent($calendarId, $calendarData, $shares, $objectData);
+ $this->logger->expects($this->once())->method('debug')->with(
+ $createsActivity ? "Activity generated for deleted calendar object in calendar $calendarId" : "Calendar object in calendar $calendarId was already in trashbin, skipping deletion activity"
+ );
+ $this->activityBackend->expects($createsActivity ? $this->once() : $this->never())->method('onTouchCalendarObject')->with(
+ Event::SUBJECT_OBJECT_DELETE,
+ $calendarData,
+ $shares,
+ $objectData
+ );
+ $this->listener->handle($event);
+ }
+
+ public static function dataForTestHandleCalendarObjectDeletedEvent(): array {
+ return [
+ [1, [], [], [], true],
+ [1, [], [], ['{' . SharingPlugin::NS_NEXTCLOUD . '}deleted-at' => 120], false],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestHandleCalendarDeletedEvent')]
+ public function testHandleCalendarDeletedEvent(int $calendarId, array $calendarData, array $shares, bool $createsActivity): void {
+ $event = new CalendarDeletedEvent($calendarId, $calendarData, $shares);
+ $this->logger->expects($this->once())->method('debug')->with(
+ $createsActivity ? "Activity generated for deleted calendar $calendarId" : "Calendar $calendarId was already in trashbin, skipping deletion activity"
+ );
+ $this->activityBackend->expects($createsActivity ? $this->once() : $this->never())->method('onCalendarDelete')->with(
+ $calendarData,
+ $shares
+ );
+ $this->listener->handle($event);
+ }
+
+ public static function dataForTestHandleCalendarDeletedEvent(): array {
+ return [
+ [1, [], [], true],
+ [1, ['{' . SharingPlugin::NS_NEXTCLOUD . '}deleted-at' => 120], [], false],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php b/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php
new file mode 100644
index 00000000000..dc3dce8a62f
--- /dev/null
+++ b/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php
@@ -0,0 +1,173 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Listener;
+
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\Events\CalendarShareUpdatedEvent;
+use OCA\DAV\Listener\CalendarContactInteractionListener;
+use OCP\Calendar\Events\CalendarObjectCreatedEvent;
+use OCP\Contacts\Events\ContactInteractedWithEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Mail\IMailer;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class CalendarContactInteractionListenerTest extends TestCase {
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private IUserSession&MockObject $userSession;
+ private Principal&MockObject $principalConnector;
+ private LoggerInterface&MockObject $logger;
+ private IMailer&MockObject $mailer;
+ private CalendarContactInteractionListener $listener;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->principalConnector = $this->createMock(Principal::class);
+ $this->mailer = $this->createMock(IMailer::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->listener = new CalendarContactInteractionListener(
+ $this->eventDispatcher,
+ $this->userSession,
+ $this->principalConnector,
+ $this->mailer,
+ $this->logger
+ );
+ }
+
+ public function testParseUnrelated(): void {
+ $event = new Event();
+ $this->eventDispatcher->expects(self::never())->method('dispatchTyped');
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleWithoutAnythingInteresting(): void {
+ $event = new CalendarShareUpdatedEvent(123, [], [], [], []);
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects(self::once())->method('getUser')->willReturn($user);
+ $this->eventDispatcher->expects(self::never())->method('dispatchTyped');
+
+ $this->listener->handle($event);
+ }
+
+ public function testParseInvalidData(): void {
+ $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => 'BEGIN:FOO']);
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects(self::once())->method('getUser')->willReturn($user);
+ $this->eventDispatcher->expects(self::never())->method('dispatchTyped');
+ $this->logger->expects(self::once())->method('warning');
+
+ $this->listener->handle($event);
+ }
+
+ public function testParseCalendarEventWithInvalidEmail(): void {
+ $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => <<<EVENT
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 2.1.3//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Vienna
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20210202T091151Z
+DTSTAMP:20210203T130231Z
+LAST-MODIFIED:20210203T130231Z
+SEQUENCE:9
+UID:b74a0c8e-93b0-447f-aed5-b679b19e874a
+DTSTART;TZID=Europe/Vienna:20210202T103000
+DTEND;TZID=Europe/Vienna:20210202T133000
+SUMMARY:tes
+ORGANIZER;CN=admin:mailto:christoph.wurst@nextcloud.com
+ATTENDEE;CN=somethingbutnotanemail;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;
+ ROLE=REQ-PARTICIPANT;RSVP=FALSE:mailto:somethingbutnotanemail
+DESCRIPTION:test
+END:VEVENT
+END:VCALENDAR
+EVENT]);
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects(self::once())->method('getUser')->willReturn($user);
+ $this->eventDispatcher->expects(self::never())->method('dispatchTyped');
+ $this->logger->expects(self::never())->method('warning');
+
+ $this->listener->handle($event);
+ }
+
+ public function testParseCalendarEvent(): void {
+ $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => <<<EVENT
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 2.1.3//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Vienna
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20210202T091151Z
+DTSTAMP:20210203T130231Z
+LAST-MODIFIED:20210203T130231Z
+SEQUENCE:9
+UID:b74a0c8e-93b0-447f-aed5-b679b19e874a
+DTSTART;TZID=Europe/Vienna:20210202T103000
+DTEND;TZID=Europe/Vienna:20210202T133000
+SUMMARY:tes
+ORGANIZER;CN=admin:mailto:christoph.wurst@nextcloud.com
+ATTENDEE;CN=user@domain.tld;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;
+ ROLE=REQ-PARTICIPANT;RSVP=FALSE:mailto:user@domain.tld
+DESCRIPTION:test
+END:VEVENT
+END:VCALENDAR
+EVENT]);
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects(self::once())->method('getUser')->willReturn($user);
+ $this->mailer->expects(self::once())->method('validateMailAddress')->willReturn(true);
+ $this->eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::equalTo((new ContactInteractedWithEvent($user))->setEmail('user@domain.tld')));
+ $this->logger->expects(self::never())->method('warning');
+
+ $this->listener->handle($event);
+ }
+}
diff --git a/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php b/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php
new file mode 100644
index 00000000000..971d113b742
--- /dev/null
+++ b/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php
@@ -0,0 +1,606 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Listener;
+
+use DateTimeImmutable;
+use InvalidArgumentException;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\CalendarObject;
+use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
+use OCA\DAV\CalDAV\Plugin;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Connector\Sabre\Server;
+use OCA\DAV\Listener\OutOfOfficeListener;
+use OCA\DAV\ServerFactory;
+use OCP\EventDispatcher\Event;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\User\Events\OutOfOfficeChangedEvent;
+use OCP\User\Events\OutOfOfficeClearedEvent;
+use OCP\User\Events\OutOfOfficeScheduledEvent;
+use OCP\User\IOutOfOfficeData;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Tree;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+/**
+ * @covers \OCA\DAV\Listener\OutOfOfficeListener
+ */
+class OutOfOfficeListenerTest extends TestCase {
+
+ private ServerFactory&MockObject $serverFactory;
+ private IConfig&MockObject $appConfig;
+ private LoggerInterface&MockObject $loggerInterface;
+ private TimezoneService&MockObject $timezoneService;
+ private OutOfOfficeListener $listener;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->serverFactory = $this->createMock(ServerFactory::class);
+ $this->appConfig = $this->createMock(IConfig::class);
+ $this->timezoneService = $this->createMock(TimezoneService::class);
+ $this->loggerInterface = $this->createMock(LoggerInterface::class);
+
+ $this->listener = new OutOfOfficeListener(
+ $this->serverFactory,
+ $this->appConfig,
+ $this->timezoneService,
+ $this->loggerInterface,
+ );
+ }
+
+ public function testHandleUnrelated(): void {
+ $event = new Event();
+
+ $this->listener->handle($event);
+
+ $this->addToAssertionCount(1);
+ }
+
+ public function testHandleSchedulingNoCalendarHome(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $event = new OutOfOfficeScheduledEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleSchedulingNoCalendarHomeNode(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeScheduledEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleSchedulingPersonalCalendarNotFound(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeScheduledEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleSchedulingWithDefaultTimezone(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $data->method('getStartDate')
+ ->willReturn((new DateTimeImmutable('2023-12-12T00:00:00Z'))->getTimestamp());
+ $data->method('getEndDate')
+ ->willReturn((new DateTimeImmutable('2023-12-13T00:00:00Z'))->getTimestamp());
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendar = $this->createMock(Calendar::class);
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user123')
+ ->willReturn('Europe/Prague');
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willReturn($calendar);
+ $calendar->expects(self::once())
+ ->method('createFile')
+ ->willReturnCallback(function ($name, $data): void {
+ $vcalendar = Reader::read($data);
+ if (!($vcalendar instanceof VCalendar)) {
+ throw new InvalidArgumentException('Calendar data should be a VCALENDAR');
+ }
+ $vevent = $vcalendar->VEVENT;
+ if ($vevent === null || !($vevent instanceof VEvent)) {
+ throw new InvalidArgumentException('Calendar data should contain a VEVENT');
+ }
+ self::assertSame('Europe/Prague', $vevent->DTSTART['TZID']?->getValue());
+ self::assertSame('Europe/Prague', $vevent->DTEND['TZID']?->getValue());
+ });
+ $event = new OutOfOfficeScheduledEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleChangeNoCalendarHome(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $event = new OutOfOfficeChangedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleChangeNoCalendarHomeNode(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeChangedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleChangePersonalCalendarNotFound(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeChangedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleChangeRecreate(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $data->method('getStartDate')
+ ->willReturn((new DateTimeImmutable('2023-12-12T00:00:00Z'))->getTimestamp());
+ $data->method('getEndDate')
+ ->willReturn((new DateTimeImmutable('2023-12-14T00:00:00Z'))->getTimestamp());
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendar = $this->createMock(Calendar::class);
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user123')
+ ->willReturn(null);
+ $this->timezoneService->expects(self::once())
+ ->method('getDefaultTimezone')
+ ->willReturn('Europe/Berlin');
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willReturn($calendar);
+ $calendar->expects(self::once())
+ ->method('getChild')
+ ->willThrowException(new NotFound());
+ $calendar->expects(self::once())
+ ->method('createFile')
+ ->willReturnCallback(function ($name, $data): void {
+ $vcalendar = Reader::read($data);
+ if (!($vcalendar instanceof VCalendar)) {
+ throw new InvalidArgumentException('Calendar data should be a VCALENDAR');
+ }
+ $vevent = $vcalendar->VEVENT;
+ if ($vevent === null || !($vevent instanceof VEvent)) {
+ throw new InvalidArgumentException('Calendar data should contain a VEVENT');
+ }
+ self::assertSame('Europe/Berlin', $vevent->DTSTART['TZID']?->getValue());
+ self::assertSame('Europe/Berlin', $vevent->DTEND['TZID']?->getValue());
+ });
+ $event = new OutOfOfficeChangedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleChangeWithoutTimezone(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $data->method('getStartDate')
+ ->willReturn((new DateTimeImmutable('2023-01-12T00:00:00Z'))->getTimestamp());
+ $data->method('getEndDate')
+ ->willReturn((new DateTimeImmutable('2023-12-14T00:00:00Z'))->getTimestamp());
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendar = $this->createMock(Calendar::class);
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willReturn($calendar);
+ $eventNode = $this->createMock(CalendarObject::class);
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user123')
+ ->willReturn(null);
+ $this->timezoneService->expects(self::once())
+ ->method('getDefaultTimezone')
+ ->willReturn('UTC');
+ $calendar->expects(self::once())
+ ->method('getChild')
+ ->willReturn($eventNode);
+ $eventNode->expects(self::once())
+ ->method('put')
+ ->willReturnCallback(function ($data): void {
+ $vcalendar = Reader::read($data);
+ if (!($vcalendar instanceof VCalendar)) {
+ throw new InvalidArgumentException('Calendar data should be a VCALENDAR');
+ }
+ $vevent = $vcalendar->VEVENT;
+ if ($vevent === null || !($vevent instanceof VEvent)) {
+ throw new InvalidArgumentException('Calendar data should contain a VEVENT');
+ }
+ // UTC datetimes are stored without a TZID
+ self::assertSame(null, $vevent->DTSTART['TZID']?->getValue());
+ self::assertSame(null, $vevent->DTEND['TZID']?->getValue());
+ });
+ $calendar->expects(self::never())
+ ->method('createFile');
+ $event = new OutOfOfficeChangedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleClearNoCalendarHome(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $event = new OutOfOfficeClearedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleClearNoCalendarHomeNode(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeClearedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleClearPersonalCalendarNotFound(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeClearedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleClearRecreate(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendar = $this->createMock(Calendar::class);
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willReturn($calendar);
+ $calendar->expects(self::once())
+ ->method('getChild')
+ ->willThrowException(new NotFound());
+ $event = new OutOfOfficeClearedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleClear(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendar = $this->createMock(Calendar::class);
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willReturn($calendar);
+ $eventNode = $this->createMock(CalendarObject::class);
+ $calendar->expects(self::once())
+ ->method('getChild')
+ ->willReturn($eventNode);
+ $eventNode->expects(self::once())
+ ->method('delete');
+ $event = new OutOfOfficeClearedEvent($data);
+
+ $this->listener->handle($event);
+ }
+}
diff --git a/apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php b/apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php
new file mode 100644
index 00000000000..1852d2709c1
--- /dev/null
+++ b/apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php
@@ -0,0 +1,230 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV\Migration;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Migration\CalDAVRemoveEmptyValue;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Server;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\VObject\InvalidDataException;
+use Test\TestCase;
+
+/**
+ * Class CalDAVRemoveEmptyValueTest
+ *
+ * @package OCA\DAV\Tests\Unit\DAV\Migration
+ * @group DB
+ */
+class CalDAVRemoveEmptyValueTest extends TestCase {
+ private LoggerInterface&MockObject $logger;
+ private CalDavBackend&MockObject $backend;
+ private IOutput&MockObject $output;
+ private string $invalid = 'BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.11.2//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+TRANSP:OPAQUE
+DTEND;VALUE=:20151223T223000Z
+LAST-MODIFIED:20151214T091032Z
+ORGANIZER;CN="User 1":mailto:user1@example.com
+UID:1234567890@example.com
+DTSTAMP:20151214T091032Z
+STATUS:CONFIRMED
+SEQUENCE:0
+SUMMARY:Ein Geburtstag
+DTSTART;VALUE=:20151223T173000Z
+X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
+CREATED;VALUE=:20151214T091032Z
+END:VEVENT
+END:VCALENDAR';
+
+ private string $valid = 'BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.11.2//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+TRANSP:OPAQUE
+DTEND:20151223T223000Z
+LAST-MODIFIED:20151214T091032Z
+ORGANIZER;CN="User 1":mailto:user1@example.com
+UID:1234567890@example.com
+DTSTAMP:20151214T091032Z
+STATUS:CONFIRMED
+SEQUENCE:0
+SUMMARY:Ein Geburtstag
+DTSTART:20151223T173000Z
+X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
+CREATED:20151214T091032Z
+END:VEVENT
+END:VCALENDAR';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->backend = $this->createMock(CalDavBackend::class);
+ $this->output = $this->createMock(IOutput::class);
+ }
+
+ public function testRunAllValid(): void {
+ /** @var CalDAVRemoveEmptyValue&MockObject $step */
+ $step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class)
+ ->setConstructorArgs([
+ Server::get(IDBConnection::class),
+ $this->backend,
+ $this->logger
+ ])
+ ->onlyMethods(['getInvalidObjects'])
+ ->getMock();
+
+ $step->expects($this->once())
+ ->method('getInvalidObjects')
+ ->willReturn([]);
+
+ $this->output->expects($this->once())
+ ->method('startProgress')
+ ->with(0);
+ $this->output->expects($this->once())
+ ->method('finishProgress');
+
+ $step->run($this->output);
+ }
+
+ public function testRunInvalid(): void {
+ /** @var CalDAVRemoveEmptyValue&MockObject $step */
+ $step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class)
+ ->setConstructorArgs([
+ Server::get(IDBConnection::class),
+ $this->backend,
+ $this->logger
+ ])
+ ->onlyMethods(['getInvalidObjects'])
+ ->getMock();
+
+ $step->expects($this->once())
+ ->method('getInvalidObjects')
+ ->willReturn([
+ ['calendarid' => '42', 'uri' => 'myuri'],
+ ]);
+
+ $this->output->expects($this->once())
+ ->method('startProgress')
+ ->with(1);
+ $this->output->expects($this->once())
+ ->method('finishProgress');
+
+ $this->backend->expects($this->exactly(1))
+ ->method('getCalendarObject')
+ ->with(42, 'myuri')
+ ->willReturn([
+ 'calendardata' => $this->invalid
+ ]);
+
+ $this->output->expects($this->exactly(1))
+ ->method('advance');
+ $this->backend->expects($this->exactly(1))
+ ->method('getDenormalizedData')
+ ->with($this->valid);
+
+ $this->backend->expects($this->exactly(1))
+ ->method('updateCalendarObject')
+ ->with(42, 'myuri', $this->valid);
+
+ $step->run($this->output);
+ }
+
+ public function testRunValid(): void {
+ /** @var CalDAVRemoveEmptyValue&MockObject $step */
+ $step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class)
+ ->setConstructorArgs([
+ Server::get(IDBConnection::class),
+ $this->backend,
+ $this->logger
+ ])
+ ->onlyMethods(['getInvalidObjects'])
+ ->getMock();
+
+ $step->expects($this->once())
+ ->method('getInvalidObjects')
+ ->willReturn([
+ ['calendarid' => '42', 'uri' => 'myuri'],
+ ]);
+
+ $this->output->expects($this->once())
+ ->method('startProgress')
+ ->with(1);
+ $this->output->expects($this->once())
+ ->method('finishProgress');
+
+
+ $this->backend->expects($this->exactly(1))
+ ->method('getCalendarObject')
+ ->with(42, 'myuri')
+ ->willReturn([
+ 'calendardata' => $this->valid
+ ]);
+
+ $this->output->expects($this->never())
+ ->method('advance');
+ $this->backend->expects($this->never())
+ ->method('getDenormalizedData');
+
+ $this->backend->expects($this->never())
+ ->method('updateCalendarObject');
+
+ $step->run($this->output);
+ }
+
+ public function testRunStillInvalid(): void {
+ /** @var CalDAVRemoveEmptyValue&MockObject $step */
+ $step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class)
+ ->setConstructorArgs([
+ Server::get(IDBConnection::class),
+ $this->backend,
+ $this->logger
+ ])
+ ->onlyMethods(['getInvalidObjects'])
+ ->getMock();
+
+ $step->expects($this->once())
+ ->method('getInvalidObjects')
+ ->willReturn([
+ ['calendarid' => '42', 'uri' => 'myuri'],
+ ]);
+
+ $this->output->expects($this->once())
+ ->method('startProgress')
+ ->with(1);
+ $this->output->expects($this->once())
+ ->method('finishProgress');
+
+
+ $this->backend->expects($this->exactly(1))
+ ->method('getCalendarObject')
+ ->with(42, 'myuri')
+ ->willReturn([
+ 'calendardata' => $this->invalid
+ ]);
+
+ $this->output->expects($this->exactly(1))
+ ->method('advance');
+ $this->backend->expects($this->exactly(1))
+ ->method('getDenormalizedData')
+ ->with($this->valid)
+ ->willThrowException(new InvalidDataException());
+
+ $this->backend->expects($this->never())
+ ->method('updateCalendarObject');
+
+ $step->run($this->output);
+ }
+}
diff --git a/apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php b/apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php
new file mode 100644
index 00000000000..667d2e39d3a
--- /dev/null
+++ b/apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Migration;
+
+use OCA\DAV\CardDAV\SyncService;
+use OCA\DAV\Migration\CreateSystemAddressBookStep;
+use OCP\Migration\IOutput;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class CreateSystemAddressBookStepTest extends TestCase {
+
+ private SyncService&MockObject $syncService;
+ private CreateSystemAddressBookStep $step;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->syncService = $this->createMock(SyncService::class);
+
+ $this->step = new CreateSystemAddressBookStep(
+ $this->syncService,
+ );
+ }
+
+ public function testGetName(): void {
+ $name = $this->step->getName();
+
+ self::assertEquals('Create system address book', $name);
+ }
+
+ public function testRun(): void {
+ $output = $this->createMock(IOutput::class);
+
+ $this->step->run($output);
+
+ $this->addToAssertionCount(1);
+ }
+
+}
diff --git a/apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php b/apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php
new file mode 100644
index 00000000000..8e7bf366cbf
--- /dev/null
+++ b/apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV\Migration;
+
+use OCA\DAV\BackgroundJob\RefreshWebcalJob;
+use OCA\DAV\Migration\RefreshWebcalJobRegistrar;
+use OCP\BackgroundJob\IJobList;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class RefreshWebcalJobRegistrarTest extends TestCase {
+ private IDBConnection&MockObject $db;
+ private IJobList&MockObject $jobList;
+ private RefreshWebcalJobRegistrar $migration;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->db = $this->createMock(IDBConnection::class);
+ $this->jobList = $this->createMock(IJobList::class);
+
+ $this->migration = new RefreshWebcalJobRegistrar($this->db, $this->jobList);
+ }
+
+ public function testGetName(): void {
+ $this->assertEquals($this->migration->getName(), 'Registering background jobs to update cache for webcal calendars');
+ }
+
+ public function testRun(): void {
+ $output = $this->createMock(IOutput::class);
+
+ $queryBuilder = $this->createMock(IQueryBuilder::class);
+ $statement = $this->createMock(IResult::class);
+
+ $this->db->expects($this->once())
+ ->method('getQueryBuilder')
+ ->willReturn($queryBuilder);
+
+ $queryBuilder->expects($this->once())
+ ->method('select')
+ ->with(['principaluri', 'uri'])
+ ->willReturn($queryBuilder);
+ $queryBuilder->expects($this->once())
+ ->method('from')
+ ->with('calendarsubscriptions')
+ ->willReturn($queryBuilder);
+ $queryBuilder->expects($this->once())
+ ->method('execute')
+ ->willReturn($statement);
+
+ $statement->expects($this->exactly(4))
+ ->method('fetch')
+ ->with(\PDO::FETCH_ASSOC)
+ ->willReturnOnConsecutiveCalls(
+ [
+ 'principaluri' => 'foo1',
+ 'uri' => 'bar1',
+ ],
+ [
+ 'principaluri' => 'foo2',
+ 'uri' => 'bar2',
+ ],
+ [
+ 'principaluri' => 'foo3',
+ 'uri' => 'bar3',
+ ],
+ null
+ );
+
+ $this->jobList->expects($this->exactly(3))
+ ->method('has')
+ ->willReturnMap([
+ [RefreshWebcalJob::class, [
+ 'principaluri' => 'foo1',
+ 'uri' => 'bar1',
+ ], false],
+ [RefreshWebcalJob::class, [
+ 'principaluri' => 'foo2',
+ 'uri' => 'bar2',
+ ], true ],
+ [RefreshWebcalJob::class, [
+ 'principaluri' => 'foo3',
+ 'uri' => 'bar3',
+ ], false],
+ ]);
+
+ $calls = [
+ [RefreshWebcalJob::class, [
+ 'principaluri' => 'foo1',
+ 'uri' => 'bar1',
+ ]],
+ [RefreshWebcalJob::class, [
+ 'principaluri' => 'foo3',
+ 'uri' => 'bar3',
+ ]]
+ ];
+ $this->jobList->expects($this->exactly(2))
+ ->method('add')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ $output->expects($this->once())
+ ->method('info')
+ ->with('Added 2 background jobs to update webcal calendars');
+
+ $this->migration->run($output);
+ }
+}
diff --git a/apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php b/apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php
new file mode 100644
index 00000000000..6f681badb8b
--- /dev/null
+++ b/apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV\Migration;
+
+use OCA\DAV\BackgroundJob\RegisterRegenerateBirthdayCalendars;
+use OCA\DAV\Migration\RegenerateBirthdayCalendars;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class RegenerateBirthdayCalendarsTest extends TestCase {
+ private IJobList&MockObject $jobList;
+ private IConfig&MockObject $config;
+ private RegenerateBirthdayCalendars $migration;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->jobList = $this->createMock(IJobList::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $this->migration = new RegenerateBirthdayCalendars($this->jobList,
+ $this->config);
+ }
+
+ public function testGetName(): void {
+ $this->assertEquals(
+ 'Regenerating birthday calendars to use new icons and fix old birthday events without year',
+ $this->migration->getName()
+ );
+ }
+
+ public function testRun(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'regeneratedBirthdayCalendarsForYearFix')
+ ->willReturn(null);
+
+ $output = $this->createMock(IOutput::class);
+ $output->expects($this->once())
+ ->method('info')
+ ->with('Adding background jobs to regenerate birthday calendar');
+
+ $this->jobList->expects($this->once())
+ ->method('add')
+ ->with(RegisterRegenerateBirthdayCalendars::class);
+
+ $this->config->expects($this->once())
+ ->method('setAppValue')
+ ->with('dav', 'regeneratedBirthdayCalendarsForYearFix', 'yes');
+
+ $this->migration->run($output);
+ }
+
+ public function testRunSecondTime(): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('dav', 'regeneratedBirthdayCalendarsForYearFix')
+ ->willReturn('yes');
+
+ $output = $this->createMock(IOutput::class);
+ $output->expects($this->once())
+ ->method('info')
+ ->with('Repair step already executed');
+
+ $this->jobList->expects($this->never())
+ ->method('add');
+
+ $this->migration->run($output);
+ }
+}
diff --git a/apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php b/apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php
new file mode 100644
index 00000000000..a9758470573
--- /dev/null
+++ b/apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php
@@ -0,0 +1,140 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV\Migration;
+
+use OCA\DAV\Migration\RemoveDeletedUsersCalendarSubscriptions;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IExpressionBuilder;
+use OCP\DB\QueryBuilder\IFunctionBuilder;
+use OCP\DB\QueryBuilder\IParameter;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\DB\QueryBuilder\IQueryFunction;
+use OCP\IDBConnection;
+use OCP\IUserManager;
+use OCP\Migration\IOutput;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class RemoveDeletedUsersCalendarSubscriptionsTest extends TestCase {
+ private IDBConnection&MockObject $dbConnection;
+ private IUserManager&MockObject $userManager;
+ private IOutput&MockObject $output;
+ private RemoveDeletedUsersCalendarSubscriptions $migration;
+
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->dbConnection = $this->createMock(IDBConnection::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->output = $this->createMock(IOutput::class);
+
+ $this->migration = new RemoveDeletedUsersCalendarSubscriptions($this->dbConnection, $this->userManager);
+ }
+
+ public function testGetName(): void {
+ $this->assertEquals(
+ 'Clean up old calendar subscriptions from deleted users that were not cleaned-up',
+ $this->migration->getName()
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestRun')]
+ public function testRun(array $subscriptions, array $userExists, int $deletions): void {
+ $qb = $this->createMock(IQueryBuilder::class);
+
+ $qb->method('select')->willReturn($qb);
+
+ $functionBuilder = $this->createMock(IFunctionBuilder::class);
+
+ $qb->method('func')->willReturn($functionBuilder);
+ $functionBuilder->method('count')->willReturn($this->createMock(IQueryFunction::class));
+
+ $qb->method('selectDistinct')
+ ->with(['id', 'principaluri'])
+ ->willReturn($qb);
+
+ $qb->method('from')
+ ->with('calendarsubscriptions')
+ ->willReturn($qb);
+
+ $qb->method('setMaxResults')
+ ->willReturn($qb);
+
+ $qb->method('setFirstResult')
+ ->willReturn($qb);
+
+ $result = $this->createMock(IResult::class);
+
+ $qb->method('execute')
+ ->willReturn($result);
+
+ $result->expects($this->once())
+ ->method('fetchOne')
+ ->willReturn(count($subscriptions));
+
+ $result
+ ->method('fetch')
+ ->willReturnOnConsecutiveCalls(...$subscriptions);
+
+ $qb->method('delete')
+ ->with('calendarsubscriptions')
+ ->willReturn($qb);
+
+ $expr = $this->createMock(IExpressionBuilder::class);
+
+ $qb->method('expr')->willReturn($expr);
+ $qb->method('createNamedParameter')->willReturn($this->createMock(IParameter::class));
+ $qb->method('where')->willReturn($qb);
+ // Only when user exists
+ $qb->expects($this->exactly($deletions))->method('executeStatement');
+
+ $this->dbConnection->method('getQueryBuilder')->willReturn($qb);
+
+
+ $this->output->expects($this->once())->method('startProgress');
+
+ $this->output->expects($subscriptions === [] ? $this->never(): $this->once())->method('advance');
+ if (count($subscriptions)) {
+ $this->userManager->method('userExists')
+ ->willReturnCallback(function (string $username) use ($userExists) {
+ return $userExists[$username];
+ });
+ }
+ $this->output->expects($this->once())->method('finishProgress');
+ $this->output->expects($this->once())->method('info')->with(sprintf('%d calendar subscriptions without an user have been cleaned up', $deletions));
+
+ $this->migration->run($this->output);
+ }
+
+ public static function dataTestRun(): array {
+ return [
+ [[], [], 0],
+ [
+ [
+ [
+ 'id' => 1,
+ 'principaluri' => 'users/principals/foo1',
+ ],
+ [
+ 'id' => 2,
+ 'principaluri' => 'users/principals/bar1',
+ ],
+ [
+ 'id' => 3,
+ 'principaluri' => 'users/principals/bar1',
+ ],
+ [],
+ ],
+ ['foo1' => true, 'bar1' => false],
+ 2
+ ],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php
new file mode 100644
index 00000000000..4f04aebb3e8
--- /dev/null
+++ b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php
@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Provisioning\Apple;
+
+use OCA\DAV\Provisioning\Apple\AppleProvisioningNode;
+use OCP\AppFramework\Utility\ITimeFactory;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\PropPatch;
+use Test\TestCase;
+
+class AppleProvisioningNodeTest extends TestCase {
+ private ITimeFactory&MockObject $timeFactory;
+ private AppleProvisioningNode $node;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->node = new AppleProvisioningNode($this->timeFactory);
+ }
+
+ public function testGetName(): void {
+ $this->assertEquals('apple-provisioning.mobileconfig', $this->node->getName());
+ }
+
+ public function testSetName(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('Renaming apple-provisioning.mobileconfig is forbidden');
+
+ $this->node->setName('foo');
+ }
+
+ public function testGetLastModified(): void {
+ $this->assertEquals(null, $this->node->getLastModified());
+ }
+
+
+ public function testDelete(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('apple-provisioning.mobileconfig may not be deleted');
+
+ $this->node->delete();
+ }
+
+ public function testGetProperties(): void {
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->willReturn(new \DateTime('2000-01-01'));
+
+ $this->assertEquals([
+ '{DAV:}getcontentlength' => 42,
+ '{DAV:}getlastmodified' => 'Sat, 01 Jan 2000 00:00:00 GMT',
+ ], $this->node->getProperties([]));
+ }
+
+
+ public function testGetPropPatch(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('apple-provisioning.mobileconfig\'s properties may not be altered.');
+
+ $propPatch = $this->createMock(PropPatch::class);
+
+ $this->node->propPatch($propPatch);
+ }
+}
diff --git a/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php
new file mode 100644
index 00000000000..58e588aa68d
--- /dev/null
+++ b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php
@@ -0,0 +1,240 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Provisioning\Apple;
+
+use OCA\DAV\Provisioning\Apple\AppleProvisioningPlugin;
+use OCA\Theming\ThemingDefaults;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Server;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+class AppleProvisioningPluginTest extends TestCase {
+ protected Server&MockObject $server;
+ protected IUserSession&MockObject $userSession;
+ protected IURLGenerator&MockObject $urlGenerator;
+ protected ThemingDefaults&MockObject $themingDefaults;
+ protected IRequest&MockObject $request;
+ protected IL10N&MockObject $l10n;
+ protected RequestInterface&MockObject $sabreRequest;
+ protected ResponseInterface&MockObject $sabreResponse;
+ protected AppleProvisioningPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->server = $this->createMock(Server::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->themingDefaults = $this->createMock(ThemingDefaults::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->l10n = $this->createMock(IL10N::class);
+
+ $this->plugin = new AppleProvisioningPlugin($this->userSession,
+ $this->urlGenerator,
+ $this->themingDefaults,
+ $this->request,
+ $this->l10n,
+ function () {
+ return 'generated-uuid';
+ }
+ );
+
+ $this->sabreRequest = $this->createMock(RequestInterface::class);
+ $this->sabreResponse = $this->createMock(ResponseInterface::class);
+ }
+
+ public function testInitialize(): void {
+ $server = $this->createMock(Server::class);
+
+ $plugin = new AppleProvisioningPlugin($this->userSession,
+ $this->urlGenerator, $this->themingDefaults, $this->request, $this->l10n,
+ function (): void {
+ });
+
+ $server->expects($this->once())
+ ->method('on')
+ ->with('method:GET', [$plugin, 'httpGet'], 90);
+
+ $plugin->initialize($server);
+ }
+
+ public function testHttpGetOnHttp(): void {
+ $this->sabreRequest->expects($this->once())
+ ->method('getPath')
+ ->with()
+ ->willReturn('provisioning/apple-provisioning.mobileconfig');
+
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->request->expects($this->once())
+ ->method('getServerProtocol')
+ ->wilLReturn('http');
+
+ $this->themingDefaults->expects($this->once())
+ ->method('getName')
+ ->willReturn('InstanceName');
+
+ $this->l10n->expects($this->once())
+ ->method('t')
+ ->with('Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS.', ['InstanceName'])
+ ->willReturn('LocalizedErrorMessage');
+
+ $this->sabreResponse->expects($this->once())
+ ->method('setStatus')
+ ->with(200);
+ $this->sabreResponse->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Type', 'text/plain; charset=utf-8');
+ $this->sabreResponse->expects($this->once())
+ ->method('setBody')
+ ->with('LocalizedErrorMessage');
+
+ $returnValue = $this->plugin->httpGet($this->sabreRequest, $this->sabreResponse);
+
+ $this->assertFalse($returnValue);
+ }
+
+ public function testHttpGetOnHttps(): void {
+ $this->sabreRequest->expects($this->once())
+ ->method('getPath')
+ ->with()
+ ->willReturn('provisioning/apple-provisioning.mobileconfig');
+
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())
+ ->method('getUID')
+ ->willReturn('userName');
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->request->expects($this->once())
+ ->method('getServerProtocol')
+ ->wilLReturn('https');
+
+ $this->urlGenerator->expects($this->once())
+ ->method('getBaseUrl')
+ ->willReturn('https://nextcloud.tld/nextcloud');
+
+ $this->themingDefaults->expects($this->once())
+ ->method('getName')
+ ->willReturn('InstanceName');
+
+ $this->l10n->expects($this->exactly(2))
+ ->method('t')
+ ->willReturnMap([
+ ['Configures a CalDAV account', [], 'LocalizedConfiguresCalDAV'],
+ ['Configures a CardDAV account', [], 'LocalizedConfiguresCardDAV'],
+ ]);
+
+ $this->sabreResponse->expects($this->once())
+ ->method('setStatus')
+ ->with(200);
+
+ $calls = [
+ ['Content-Disposition', 'attachment; filename="userName-apple-provisioning.mobileconfig"'],
+ ['Content-Type', 'application/xml; charset=utf-8'],
+ ];
+ $this->sabreResponse->expects($this->exactly(2))
+ ->method('setHeader')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+ $this->sabreResponse->expects($this->once())
+ ->method('setBody')
+ ->with(<<<EOF
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>PayloadContent</key>
+ <array>
+ <dict>
+ <key>CalDAVAccountDescription</key>
+ <string>InstanceName</string>
+ <key>CalDAVHostName</key>
+ <string>nextcloud.tld</string>
+ <key>CalDAVUsername</key>
+ <string>userName</string>
+ <key>CalDAVUseSSL</key>
+ <true/>
+ <key>CalDAVPort</key>
+ <integer>443</integer>
+ <key>PayloadDescription</key>
+ <string>LocalizedConfiguresCalDAV</string>
+ <key>PayloadDisplayName</key>
+ <string>InstanceName CalDAV</string>
+ <key>PayloadIdentifier</key>
+ <string>tld.nextcloud.generated-uuid</string>
+ <key>PayloadType</key>
+ <string>com.apple.caldav.account</string>
+ <key>PayloadUUID</key>
+ <string>generated-uuid</string>
+ <key>PayloadVersion</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>CardDAVAccountDescription</key>
+ <string>InstanceName</string>
+ <key>CardDAVHostName</key>
+ <string>nextcloud.tld</string>
+ <key>CardDAVUsername</key>
+ <string>userName</string>
+ <key>CardDAVUseSSL</key>
+ <true/>
+ <key>CardDAVPort</key>
+ <integer>443</integer>
+ <key>PayloadDescription</key>
+ <string>LocalizedConfiguresCardDAV</string>
+ <key>PayloadDisplayName</key>
+ <string>InstanceName CardDAV</string>
+ <key>PayloadIdentifier</key>
+ <string>tld.nextcloud.generated-uuid</string>
+ <key>PayloadType</key>
+ <string>com.apple.carddav.account</string>
+ <key>PayloadUUID</key>
+ <string>generated-uuid</string>
+ <key>PayloadVersion</key>
+ <integer>1</integer>
+ </dict>
+ </array>
+ <key>PayloadDisplayName</key>
+ <string>InstanceName</string>
+ <key>PayloadIdentifier</key>
+ <string>tld.nextcloud.generated-uuid</string>
+ <key>PayloadRemovalDisallowed</key>
+ <false/>
+ <key>PayloadType</key>
+ <string>Configuration</string>
+ <key>PayloadUUID</key>
+ <string>generated-uuid</string>
+ <key>PayloadVersion</key>
+ <integer>1</integer>
+</dict>
+</plist>
+
+EOF
+ );
+
+ $returnValue = $this->plugin->httpGet($this->sabreRequest, $this->sabreResponse);
+
+ $this->assertFalse($returnValue);
+ }
+}
diff --git a/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php b/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php
new file mode 100644
index 00000000000..f4dc13a5c06
--- /dev/null
+++ b/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php
@@ -0,0 +1,259 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\Search\ContactsSearchProvider;
+use OCP\App\IAppManager;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\Search\ISearchQuery;
+use OCP\Search\SearchResult;
+use OCP\Search\SearchResultEntry;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+class ContactsSearchProviderTest extends TestCase {
+ private IAppManager&MockObject $appManager;
+ private IL10N&MockObject $l10n;
+ private IURLGenerator&MockObject $urlGenerator;
+ private CardDavBackend&MockObject $backend;
+ private ContactsSearchProvider $provider;
+
+ private string $vcardTest0 = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'UID:Test' . PHP_EOL
+ . 'FN:FN of Test' . PHP_EOL
+ . 'N:Test;;;;' . PHP_EOL
+ . 'EMAIL:forrestgump@example.com' . PHP_EOL
+ . 'END:VCARD';
+
+ private string $vcardTest1 = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'PHOTO;ENCODING=b;TYPE=image/jpeg:' . PHP_EOL
+ . 'UID:Test2' . PHP_EOL
+ . 'FN:FN of Test2' . PHP_EOL
+ . 'N:Test2;;;;' . PHP_EOL
+ . 'END:VCARD';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->backend = $this->createMock(CardDavBackend::class);
+
+ $this->provider = new ContactsSearchProvider(
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend
+ );
+ }
+
+ public function testGetId(): void {
+ $this->assertEquals('contacts', $this->provider->getId());
+ }
+
+ public function testGetName(): void {
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->with('Contacts')
+ ->willReturnArgument(0);
+
+ $this->assertEquals('Contacts', $this->provider->getName());
+ }
+
+ public function testSearchAppDisabled(): void {
+ $user = $this->createMock(IUser::class);
+ $query = $this->createMock(ISearchQuery::class);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('contacts', $user)
+ ->willReturn(false);
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->with('Contacts')
+ ->willReturnArgument(0);
+ $this->backend->expects($this->never())
+ ->method('getAddressBooksForUser');
+ $this->backend->expects($this->never())
+ ->method('searchPrincipalUri');
+
+ $actual = $this->provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Contacts', $data['name']);
+ $this->assertEmpty($data['entries']);
+ $this->assertFalse($data['isPaginated']);
+ $this->assertNull($data['cursor']);
+ }
+
+ public function testSearch(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('john.doe');
+ $query = $this->createMock(ISearchQuery::class);
+ $query->method('getTerm')->willReturn('search term');
+ $query->method('getLimit')->willReturn(5);
+ $query->method('getCursor')->willReturn(20);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('contacts', $user)
+ ->willReturn(true);
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->with('Contacts')
+ ->willReturnArgument(0);
+
+ $this->backend->expects($this->once())
+ ->method('getAddressBooksForUser')
+ ->with('principals/users/john.doe')
+ ->willReturn([
+ [
+ 'id' => 99,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'addressbook-uri-99',
+ ], [
+ 'id' => 123,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'addressbook-uri-123',
+ ]
+ ]);
+ $this->backend->expects($this->once())
+ ->method('searchPrincipalUri')
+ ->with('principals/users/john.doe', '',
+ [
+ 'N',
+ 'FN',
+ 'NICKNAME',
+ 'EMAIL',
+ 'TEL',
+ 'ADR',
+ 'TITLE',
+ 'ORG',
+ 'NOTE',
+ ],
+ ['limit' => 5, 'offset' => 20, 'since' => null, 'until' => null, 'person' => null, 'company' => null])
+ ->willReturn([
+ [
+ 'addressbookid' => 99,
+ 'uri' => 'vcard0.vcf',
+ 'carddata' => $this->vcardTest0,
+ ],
+ [
+ 'addressbookid' => 123,
+ 'uri' => 'vcard1.vcf',
+ 'carddata' => $this->vcardTest1,
+ ],
+ ]);
+
+ $provider = $this->getMockBuilder(ContactsSearchProvider::class)
+ ->setConstructorArgs([
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend,
+ ])
+ ->onlyMethods([
+ 'getDavUrlForContact',
+ 'getDeepLinkToContactsApp',
+ 'generateSubline',
+ ])
+ ->getMock();
+
+ $provider->expects($this->once())
+ ->method('getDavUrlForContact')
+ ->with('principals/users/john.doe', 'addressbook-uri-123', 'vcard1.vcf')
+ ->willReturn('absolute-thumbnail-url');
+
+ $provider->expects($this->exactly(2))
+ ->method('generateSubline')
+ ->willReturn('subline');
+ $provider->expects($this->exactly(2))
+ ->method('getDeepLinkToContactsApp')
+ ->willReturnMap([
+ ['addressbook-uri-99', 'Test', 'deep-link-to-contacts'],
+ ['addressbook-uri-123', 'Test2', 'deep-link-to-contacts'],
+ ]);
+
+ $actual = $provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Contacts', $data['name']);
+ $this->assertCount(2, $data['entries']);
+ $this->assertTrue($data['isPaginated']);
+ $this->assertEquals(22, $data['cursor']);
+
+ $result0 = $data['entries'][0];
+ $result0Data = $result0->jsonSerialize();
+ $result1 = $data['entries'][1];
+ $result1Data = $result1->jsonSerialize();
+
+ $this->assertInstanceOf(SearchResultEntry::class, $result0);
+ $this->assertEquals('', $result0Data['thumbnailUrl']);
+ $this->assertEquals('FN of Test', $result0Data['title']);
+ $this->assertEquals('subline', $result0Data['subline']);
+ $this->assertEquals('deep-link-to-contacts', $result0Data['resourceUrl']);
+ $this->assertEquals('icon-contacts-dark', $result0Data['icon']);
+ $this->assertTrue($result0Data['rounded']);
+
+ $this->assertInstanceOf(SearchResultEntry::class, $result1);
+ $this->assertEquals('absolute-thumbnail-url?photo', $result1Data['thumbnailUrl']);
+ $this->assertEquals('FN of Test2', $result1Data['title']);
+ $this->assertEquals('subline', $result1Data['subline']);
+ $this->assertEquals('deep-link-to-contacts', $result1Data['resourceUrl']);
+ $this->assertEquals('icon-contacts-dark', $result1Data['icon']);
+ $this->assertTrue($result1Data['rounded']);
+ }
+
+ public function testGetDavUrlForContact(): void {
+ $this->urlGenerator->expects($this->once())
+ ->method('linkTo')
+ ->with('', 'remote.php')
+ ->willReturn('link-to-remote.php');
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf')
+ ->willReturn('absolute-url-link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf');
+
+ $actual = self::invokePrivate($this->provider, 'getDavUrlForContact', ['principals/users/john.doe', 'foo', 'bar.vcf']);
+
+ $this->assertEquals('absolute-url-link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf', $actual);
+ }
+
+ public function testGetDeepLinkToContactsApp(): void {
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('contacts.contacts.direct', ['contact' => 'uid123~uri-john.doe'])
+ ->willReturn('link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe');
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe')
+ ->willReturn('absolute-url-link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe');
+
+ $actual = self::invokePrivate($this->provider, 'getDeepLinkToContactsApp', ['uri-john.doe', 'uid123']);
+ $this->assertEquals('absolute-url-link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe', $actual);
+ }
+
+ public function testGenerateSubline(): void {
+ $vCard0 = Reader::read($this->vcardTest0);
+ $vCard1 = Reader::read($this->vcardTest1);
+
+ $actual1 = self::invokePrivate($this->provider, 'generateSubline', [$vCard0]);
+ $actual2 = self::invokePrivate($this->provider, 'generateSubline', [$vCard1]);
+
+ $this->assertEquals('forrestgump@example.com', $actual1);
+ $this->assertEquals('', $actual2);
+ }
+}
diff --git a/apps/dav/tests/unit/Search/EventsSearchProviderTest.php b/apps/dav/tests/unit/Search/EventsSearchProviderTest.php
new file mode 100644
index 00000000000..d5d536fd201
--- /dev/null
+++ b/apps/dav/tests/unit/Search/EventsSearchProviderTest.php
@@ -0,0 +1,450 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Search;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Search\EventsSearchProvider;
+use OCP\App\IAppManager;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\Search\IFilter;
+use OCP\Search\ISearchQuery;
+use OCP\Search\SearchResult;
+use OCP\Search\SearchResultEntry;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+class EventsSearchProviderTest extends TestCase {
+ private IAppManager&MockObject $appManager;
+ private IL10N&MockObject $l10n;
+ private IURLGenerator&MockObject $urlGenerator;
+ private CalDavBackend&MockObject $backend;
+ private EventsSearchProvider $provider;
+
+ // NO SUMMARY
+ private static string $vEvent0 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20161004T144433Z' . PHP_EOL
+ . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL
+ . 'DTEND;VALUE=DATE:20161008' . PHP_EOL
+ . 'TRANSP:TRANSPARENT' . PHP_EOL
+ . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL
+ . 'DTSTAMP:20161004T144437Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // TIMED SAME DAY
+ private static string $vEvent1 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Tests//' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VTIMEZONE' . PHP_EOL
+ . 'TZID:Europe/Berlin' . PHP_EOL
+ . 'BEGIN:DAYLIGHT' . PHP_EOL
+ . 'TZOFFSETFROM:+0100' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19810329T020000' . PHP_EOL
+ . 'TZNAME:GMT+2' . PHP_EOL
+ . 'TZOFFSETTO:+0200' . PHP_EOL
+ . 'END:DAYLIGHT' . PHP_EOL
+ . 'BEGIN:STANDARD' . PHP_EOL
+ . 'TZOFFSETFROM:+0200' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19961027T030000' . PHP_EOL
+ . 'TZNAME:GMT+1' . PHP_EOL
+ . 'TZOFFSETTO:+0100' . PHP_EOL
+ . 'END:STANDARD' . PHP_EOL
+ . 'END:VTIMEZONE' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20160809T163629Z' . PHP_EOL
+ . 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02' . PHP_EOL
+ . 'DTEND;TZID=Europe/Berlin:20160816T100000' . PHP_EOL
+ . 'TRANSP:OPAQUE' . PHP_EOL
+ . 'SUMMARY:Test Europe Berlin' . PHP_EOL
+ . 'DTSTART;TZID=Europe/Berlin:20160816T090000' . PHP_EOL
+ . 'DTSTAMP:20160809T163632Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // TIMED DIFFERENT DAY
+ private static string $vEvent2 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Tests//' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VTIMEZONE' . PHP_EOL
+ . 'TZID:Europe/Berlin' . PHP_EOL
+ . 'BEGIN:DAYLIGHT' . PHP_EOL
+ . 'TZOFFSETFROM:+0100' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19810329T020000' . PHP_EOL
+ . 'TZNAME:GMT+2' . PHP_EOL
+ . 'TZOFFSETTO:+0200' . PHP_EOL
+ . 'END:DAYLIGHT' . PHP_EOL
+ . 'BEGIN:STANDARD' . PHP_EOL
+ . 'TZOFFSETFROM:+0200' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19961027T030000' . PHP_EOL
+ . 'TZNAME:GMT+1' . PHP_EOL
+ . 'TZOFFSETTO:+0100' . PHP_EOL
+ . 'END:STANDARD' . PHP_EOL
+ . 'END:VTIMEZONE' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20160809T163629Z' . PHP_EOL
+ . 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02' . PHP_EOL
+ . 'DTEND;TZID=Europe/Berlin:20160817T100000' . PHP_EOL
+ . 'TRANSP:OPAQUE' . PHP_EOL
+ . 'SUMMARY:Test Europe Berlin' . PHP_EOL
+ . 'DTSTART;TZID=Europe/Berlin:20160816T090000' . PHP_EOL
+ . 'DTSTAMP:20160809T163632Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // ALL-DAY ONE-DAY
+ private static string $vEvent3 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20161004T144433Z' . PHP_EOL
+ . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL
+ . 'DTEND;VALUE=DATE:20161006' . PHP_EOL
+ . 'TRANSP:TRANSPARENT' . PHP_EOL
+ . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL
+ . 'DTSTAMP:20161004T144437Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // ALL-DAY MULTIPLE DAYS
+ private static string $vEvent4 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20161004T144433Z' . PHP_EOL
+ . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL
+ . 'DTEND;VALUE=DATE:20161008' . PHP_EOL
+ . 'TRANSP:TRANSPARENT' . PHP_EOL
+ . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL
+ . 'DTSTAMP:20161004T144437Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // DURATION
+ private static string $vEvent5 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20161004T144433Z' . PHP_EOL
+ . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL
+ . 'DURATION:P5D' . PHP_EOL
+ . 'TRANSP:TRANSPARENT' . PHP_EOL
+ . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL
+ . 'DTSTAMP:20161004T144437Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // NO DTEND - DATE
+ private static string $vEvent6 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20161004T144433Z' . PHP_EOL
+ . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL
+ . 'TRANSP:TRANSPARENT' . PHP_EOL
+ . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL
+ . 'DTSTAMP:20161004T144437Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // NO DTEND - DATE-TIME
+ private static string $vEvent7 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Tests//' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VTIMEZONE' . PHP_EOL
+ . 'TZID:Europe/Berlin' . PHP_EOL
+ . 'BEGIN:DAYLIGHT' . PHP_EOL
+ . 'TZOFFSETFROM:+0100' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19810329T020000' . PHP_EOL
+ . 'TZNAME:GMT+2' . PHP_EOL
+ . 'TZOFFSETTO:+0200' . PHP_EOL
+ . 'END:DAYLIGHT' . PHP_EOL
+ . 'BEGIN:STANDARD' . PHP_EOL
+ . 'TZOFFSETFROM:+0200' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19961027T030000' . PHP_EOL
+ . 'TZNAME:GMT+1' . PHP_EOL
+ . 'TZOFFSETTO:+0100' . PHP_EOL
+ . 'END:STANDARD' . PHP_EOL
+ . 'END:VTIMEZONE' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20160809T163629Z' . PHP_EOL
+ . 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02' . PHP_EOL
+ . 'TRANSP:OPAQUE' . PHP_EOL
+ . 'SUMMARY:Test Europe Berlin' . PHP_EOL
+ . 'DTSTART;TZID=Europe/Berlin:20160816T090000' . PHP_EOL
+ . 'DTSTAMP:20160809T163632Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->backend = $this->createMock(CalDavBackend::class);
+
+ $this->provider = new EventsSearchProvider(
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend
+ );
+ }
+
+ public function testGetId(): void {
+ $this->assertEquals('calendar', $this->provider->getId());
+ }
+
+ public function testGetName(): void {
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->with('Events')
+ ->willReturnArgument(0);
+
+ $this->assertEquals('Events', $this->provider->getName());
+ }
+
+ public function testSearchAppDisabled(): void {
+ $user = $this->createMock(IUser::class);
+ $query = $this->createMock(ISearchQuery::class);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('calendar', $user)
+ ->willReturn(false);
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->willReturnArgument(0);
+ $this->backend->expects($this->never())
+ ->method('getCalendarsForUser');
+ $this->backend->expects($this->never())
+ ->method('getSubscriptionsForUser');
+ $this->backend->expects($this->never())
+ ->method('searchPrincipalUri');
+
+ $actual = $this->provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Events', $data['name']);
+ $this->assertEmpty($data['entries']);
+ $this->assertFalse($data['isPaginated']);
+ $this->assertNull($data['cursor']);
+ }
+
+ public function testSearch(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('john.doe');
+ $query = $this->createMock(ISearchQuery::class);
+ $seachTermFilter = $this->createMock(IFilter::class);
+ $query->method('getFilter')->willReturnCallback(function ($name) use ($seachTermFilter) {
+ return match ($name) {
+ 'term' => $seachTermFilter,
+ default => null,
+ };
+ });
+ $seachTermFilter->method('get')->willReturn('search term');
+ $query->method('getLimit')->willReturn(5);
+ $query->method('getCursor')->willReturn(20);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('calendar', $user)
+ ->willReturn(true);
+ $this->l10n->method('t')->willReturnArgument(0);
+
+ $this->backend->expects($this->once())
+ ->method('getCalendarsForUser')
+ ->with('principals/users/john.doe')
+ ->willReturn([
+ [
+ 'id' => 99,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'calendar-uri-99',
+ ], [
+ 'id' => 123,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'calendar-uri-123',
+ ]
+ ]);
+ $this->backend->expects($this->once())
+ ->method('getSubscriptionsForUser')
+ ->with('principals/users/john.doe')
+ ->willReturn([
+ [
+ 'id' => 1337,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'subscription-uri-1337',
+ ]
+ ]);
+ $this->backend->expects($this->once())
+ ->method('searchPrincipalUri')
+ ->with('principals/users/john.doe', 'search term', ['VEVENT'],
+ ['SUMMARY', 'LOCATION', 'DESCRIPTION', 'ATTENDEE', 'ORGANIZER', 'CATEGORIES'],
+ ['ATTENDEE' => ['CN'], 'ORGANIZER' => ['CN']],
+ ['limit' => 5, 'offset' => 20, 'timerange' => ['start' => null, 'end' => null]])
+ ->willReturn([
+ [
+ 'calendarid' => 99,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
+ 'uri' => 'event0.ics',
+ 'calendardata' => self::$vEvent0,
+ ],
+ [
+ 'calendarid' => 123,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
+ 'uri' => 'event1.ics',
+ 'calendardata' => self::$vEvent1,
+ ],
+ [
+ 'calendarid' => 1337,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION,
+ 'uri' => 'event2.ics',
+ 'calendardata' => self::$vEvent2,
+ ]
+ ]);
+
+ $provider = $this->getMockBuilder(EventsSearchProvider::class)
+ ->setConstructorArgs([
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend,
+ ])
+ ->onlyMethods([
+ 'getDeepLinkToCalendarApp',
+ 'generateSubline',
+ ])
+ ->getMock();
+
+ $provider->expects($this->exactly(3))
+ ->method('generateSubline')
+ ->willReturn('subline');
+ $provider->expects($this->exactly(3))
+ ->method('getDeepLinkToCalendarApp')
+ ->willReturnMap([
+ ['principals/users/john.doe', 'calendar-uri-99', 'event0.ics', 'deep-link-to-calendar'],
+ ['principals/users/john.doe', 'calendar-uri-123', 'event1.ics', 'deep-link-to-calendar'],
+ ['principals/users/john.doe', 'subscription-uri-1337', 'event2.ics', 'deep-link-to-calendar']
+ ]);
+
+ $actual = $provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Events', $data['name']);
+ $this->assertCount(3, $data['entries']);
+ $this->assertTrue($data['isPaginated']);
+ $this->assertEquals(23, $data['cursor']);
+
+ $result0 = $data['entries'][0];
+ $result0Data = $result0->jsonSerialize();
+ $result1 = $data['entries'][1];
+ $result1Data = $result1->jsonSerialize();
+ $result2 = $data['entries'][2];
+ $result2Data = $result2->jsonSerialize();
+
+ $this->assertInstanceOf(SearchResultEntry::class, $result0);
+ $this->assertEmpty($result0Data['thumbnailUrl']);
+ $this->assertEquals('Untitled event', $result0Data['title']);
+ $this->assertEquals('subline', $result0Data['subline']);
+ $this->assertEquals('deep-link-to-calendar', $result0Data['resourceUrl']);
+ $this->assertEquals('icon-calendar-dark', $result0Data['icon']);
+ $this->assertFalse($result0Data['rounded']);
+
+ $this->assertInstanceOf(SearchResultEntry::class, $result1);
+ $this->assertEmpty($result1Data['thumbnailUrl']);
+ $this->assertEquals('Test Europe Berlin', $result1Data['title']);
+ $this->assertEquals('subline', $result1Data['subline']);
+ $this->assertEquals('deep-link-to-calendar', $result1Data['resourceUrl']);
+ $this->assertEquals('icon-calendar-dark', $result1Data['icon']);
+ $this->assertFalse($result1Data['rounded']);
+
+ $this->assertInstanceOf(SearchResultEntry::class, $result2);
+ $this->assertEmpty($result2Data['thumbnailUrl']);
+ $this->assertEquals('Test Europe Berlin', $result2Data['title']);
+ $this->assertEquals('subline', $result2Data['subline']);
+ $this->assertEquals('deep-link-to-calendar', $result2Data['resourceUrl']);
+ $this->assertEquals('icon-calendar-dark', $result2Data['icon']);
+ $this->assertFalse($result2Data['rounded']);
+ }
+
+ public function testGetDeepLinkToCalendarApp(): void {
+ $this->urlGenerator->expects($this->once())
+ ->method('linkTo')
+ ->with('', 'remote.php')
+ ->willReturn('link-to-remote.php');
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('calendar.view.index')
+ ->willReturn('link-to-route-calendar/');
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('link-to-route-calendar/edit/bGluay10by1yZW1vdGUucGhwL2Rhdi9jYWxlbmRhcnMvam9obi5kb2UvZm9vL2Jhci5pY3M=')
+ ->willReturn('absolute-url-to-route');
+
+ $actual = self::invokePrivate($this->provider, 'getDeepLinkToCalendarApp', ['principals/users/john.doe', 'foo', 'bar.ics']);
+
+ $this->assertEquals('absolute-url-to-route', $actual);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('generateSublineDataProvider')]
+ public function testGenerateSubline(string $ics, string $expectedSubline): void {
+ $vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING);
+ $eventComponent = $vCalendar->VEVENT;
+
+ $this->l10n->method('l')
+ ->willReturnCallback(static function (string $type, \DateTime $date, $_):string {
+ if ($type === 'time') {
+ return $date->format('H:i');
+ }
+
+ return $date->format('m-d');
+ });
+
+ $actual = self::invokePrivate($this->provider, 'generateSubline', [$eventComponent]);
+ $this->assertEquals($expectedSubline, $actual);
+ }
+
+ public static function generateSublineDataProvider(): array {
+ return [
+ [self::$vEvent1, '08-16 09:00 - 10:00'],
+ [self::$vEvent2, '08-16 09:00 - 08-17 10:00'],
+ [self::$vEvent3, '10-05'],
+ [self::$vEvent4, '10-05 - 10-07'],
+ [self::$vEvent5, '10-05 - 10-09'],
+ [self::$vEvent6, '10-05'],
+ [self::$vEvent7, '08-16 09:00 - 09:00'],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/Search/TasksSearchProviderTest.php b/apps/dav/tests/unit/Search/TasksSearchProviderTest.php
new file mode 100644
index 00000000000..7f9a2842de9
--- /dev/null
+++ b/apps/dav/tests/unit/Search/TasksSearchProviderTest.php
@@ -0,0 +1,313 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Search;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Search\TasksSearchProvider;
+use OCP\App\IAppManager;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\Search\ISearchQuery;
+use OCP\Search\SearchResult;
+use OCP\Search\SearchResultEntry;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+class TasksSearchProviderTest extends TestCase {
+ private IAppManager&MockObject $appManager;
+ private IL10N&MockObject $l10n;
+ private IURLGenerator&MockObject $urlGenerator;
+ private CalDavBackend&MockObject $backend;
+ private TasksSearchProvider $provider;
+
+ // NO DUE NOR COMPLETED NOR SUMMARY
+ private static string $vTodo0 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'PRODID:TEST' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'BEGIN:VTODO' . PHP_EOL
+ . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL
+ . 'DTSTAMP:20070313T123432Z' . PHP_EOL
+ . 'STATUS:NEEDS-ACTION' . PHP_EOL
+ . 'END:VTODO' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // DUE AND COMPLETED
+ private static string $vTodo1 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'PRODID:TEST' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'BEGIN:VTODO' . PHP_EOL
+ . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL
+ . 'DTSTAMP:20070313T123432Z' . PHP_EOL
+ . 'COMPLETED:20070707T100000Z' . PHP_EOL
+ . 'DUE;VALUE=DATE:20070501' . PHP_EOL
+ . 'SUMMARY:Task title' . PHP_EOL
+ . 'STATUS:NEEDS-ACTION' . PHP_EOL
+ . 'END:VTODO' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // COMPLETED ONLY
+ private static string $vTodo2 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'PRODID:TEST' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'BEGIN:VTODO' . PHP_EOL
+ . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL
+ . 'DTSTAMP:20070313T123432Z' . PHP_EOL
+ . 'COMPLETED:20070707T100000Z' . PHP_EOL
+ . 'SUMMARY:Task title' . PHP_EOL
+ . 'STATUS:NEEDS-ACTION' . PHP_EOL
+ . 'END:VTODO' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // DUE DATE
+ private static string $vTodo3 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'PRODID:TEST' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'BEGIN:VTODO' . PHP_EOL
+ . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL
+ . 'DTSTAMP:20070313T123432Z' . PHP_EOL
+ . 'DUE;VALUE=DATE:20070501' . PHP_EOL
+ . 'SUMMARY:Task title' . PHP_EOL
+ . 'STATUS:NEEDS-ACTION' . PHP_EOL
+ . 'END:VTODO' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ // DUE DATETIME
+ private static string $vTodo4 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'PRODID:TEST' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'BEGIN:VTODO' . PHP_EOL
+ . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL
+ . 'DTSTAMP:20070313T123432Z' . PHP_EOL
+ . 'DUE:20070709T130000Z' . PHP_EOL
+ . 'SUMMARY:Task title' . PHP_EOL
+ . 'STATUS:NEEDS-ACTION' . PHP_EOL
+ . 'END:VTODO' . PHP_EOL
+ . 'END:VCALENDAR';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->backend = $this->createMock(CalDavBackend::class);
+
+ $this->provider = new TasksSearchProvider(
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend
+ );
+ }
+
+ public function testGetId(): void {
+ $this->assertEquals('tasks', $this->provider->getId());
+ }
+
+ public function testGetName(): void {
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->with('Tasks')
+ ->willReturnArgument(0);
+
+ $this->assertEquals('Tasks', $this->provider->getName());
+ }
+
+ public function testSearchAppDisabled(): void {
+ $user = $this->createMock(IUser::class);
+ $query = $this->createMock(ISearchQuery::class);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('tasks', $user)
+ ->willReturn(false);
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->willReturnArgument(0);
+ $this->backend->expects($this->never())
+ ->method('getCalendarsForUser');
+ $this->backend->expects($this->never())
+ ->method('getSubscriptionsForUser');
+ $this->backend->expects($this->never())
+ ->method('searchPrincipalUri');
+
+ $actual = $this->provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Tasks', $data['name']);
+ $this->assertEmpty($data['entries']);
+ $this->assertFalse($data['isPaginated']);
+ $this->assertNull($data['cursor']);
+ }
+
+ public function testSearch(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('john.doe');
+ $query = $this->createMock(ISearchQuery::class);
+ $query->method('getTerm')->willReturn('search term');
+ $query->method('getLimit')->willReturn(5);
+ $query->method('getCursor')->willReturn(20);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('tasks', $user)
+ ->willReturn(true);
+ $this->l10n->method('t')->willReturnArgument(0);
+
+ $this->backend->expects($this->once())
+ ->method('getCalendarsForUser')
+ ->with('principals/users/john.doe')
+ ->willReturn([
+ [
+ 'id' => 99,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'calendar-uri-99',
+ ], [
+ 'id' => 123,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'calendar-uri-123',
+ ]
+ ]);
+ $this->backend->expects($this->once())
+ ->method('getSubscriptionsForUser')
+ ->with('principals/users/john.doe')
+ ->willReturn([
+ [
+ 'id' => 1337,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'subscription-uri-1337',
+ ]
+ ]);
+ $this->backend->expects($this->once())
+ ->method('searchPrincipalUri')
+ ->with('principals/users/john.doe', '', ['VTODO'],
+ ['SUMMARY', 'DESCRIPTION', 'CATEGORIES'],
+ [],
+ ['limit' => 5, 'offset' => 20, 'since' => null, 'until' => null])
+ ->willReturn([
+ [
+ 'calendarid' => 99,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
+ 'uri' => 'todo0.ics',
+ 'calendardata' => self::$vTodo0,
+ ],
+ [
+ 'calendarid' => 123,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
+ 'uri' => 'todo1.ics',
+ 'calendardata' => self::$vTodo1,
+ ],
+ [
+ 'calendarid' => 1337,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION,
+ 'uri' => 'todo2.ics',
+ 'calendardata' => self::$vTodo2,
+ ]
+ ]);
+
+ $provider = $this->getMockBuilder(TasksSearchProvider::class)
+ ->setConstructorArgs([
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend,
+ ])
+ ->onlyMethods([
+ 'getDeepLinkToTasksApp',
+ 'generateSubline',
+ ])
+ ->getMock();
+
+ $provider->expects($this->exactly(3))
+ ->method('generateSubline')
+ ->willReturn('subline');
+ $provider->expects($this->exactly(3))
+ ->method('getDeepLinkToTasksApp')
+ ->willReturnMap([
+ ['calendar-uri-99', 'todo0.ics', 'deep-link-to-tasks'],
+ ['calendar-uri-123', 'todo1.ics', 'deep-link-to-tasks'],
+ ['subscription-uri-1337', 'todo2.ics', 'deep-link-to-tasks']
+ ]);
+
+ $actual = $provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Tasks', $data['name']);
+ $this->assertCount(3, $data['entries']);
+ $this->assertTrue($data['isPaginated']);
+ $this->assertEquals(23, $data['cursor']);
+
+ $result0 = $data['entries'][0];
+ $result0Data = $result0->jsonSerialize();
+ $result1 = $data['entries'][1];
+ $result1Data = $result1->jsonSerialize();
+ $result2 = $data['entries'][2];
+ $result2Data = $result2->jsonSerialize();
+
+ $this->assertInstanceOf(SearchResultEntry::class, $result0);
+ $this->assertEmpty($result0Data['thumbnailUrl']);
+ $this->assertEquals('Untitled task', $result0Data['title']);
+ $this->assertEquals('subline', $result0Data['subline']);
+ $this->assertEquals('deep-link-to-tasks', $result0Data['resourceUrl']);
+ $this->assertEquals('icon-checkmark', $result0Data['icon']);
+ $this->assertFalse($result0Data['rounded']);
+
+ $this->assertInstanceOf(SearchResultEntry::class, $result1);
+ $this->assertEmpty($result1Data['thumbnailUrl']);
+ $this->assertEquals('Task title', $result1Data['title']);
+ $this->assertEquals('subline', $result1Data['subline']);
+ $this->assertEquals('deep-link-to-tasks', $result1Data['resourceUrl']);
+ $this->assertEquals('icon-checkmark', $result1Data['icon']);
+ $this->assertFalse($result1Data['rounded']);
+
+ $this->assertInstanceOf(SearchResultEntry::class, $result2);
+ $this->assertEmpty($result2Data['thumbnailUrl']);
+ $this->assertEquals('Task title', $result2Data['title']);
+ $this->assertEquals('subline', $result2Data['subline']);
+ $this->assertEquals('deep-link-to-tasks', $result2Data['resourceUrl']);
+ $this->assertEquals('icon-checkmark', $result2Data['icon']);
+ $this->assertFalse($result2Data['rounded']);
+ }
+
+ public function testGetDeepLinkToTasksApp(): void {
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('tasks.page.index')
+ ->willReturn('link-to-route-tasks.index');
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('link-to-route-tasks.indexcalendars/uri-john.doe/tasks/task-uri.ics')
+ ->willReturn('absolute-url-link-to-route-tasks.indexcalendars/uri-john.doe/tasks/task-uri.ics');
+
+ $actual = self::invokePrivate($this->provider, 'getDeepLinkToTasksApp', ['uri-john.doe', 'task-uri.ics']);
+ $this->assertEquals('absolute-url-link-to-route-tasks.indexcalendars/uri-john.doe/tasks/task-uri.ics', $actual);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('generateSublineDataProvider')]
+ public function testGenerateSubline(string $ics, string $expectedSubline): void {
+ $vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING);
+ $taskComponent = $vCalendar->VTODO;
+
+ $this->l10n->method('t')->willReturnArgument(0);
+ $this->l10n->method('l')->willReturnArgument(0);
+
+ $actual = self::invokePrivate($this->provider, 'generateSubline', [$taskComponent]);
+ $this->assertEquals($expectedSubline, $actual);
+ }
+
+ public static function generateSublineDataProvider(): array {
+ return [
+ [self::$vTodo0, ''],
+ [self::$vTodo1, 'Completed on %s'],
+ [self::$vTodo2, 'Completed on %s'],
+ [self::$vTodo3, 'Due on %s'],
+ [self::$vTodo4, 'Due on %s by %s'],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/ServerTest.php b/apps/dav/tests/unit/ServerTest.php
new file mode 100644
index 00000000000..9ffe86d3053
--- /dev/null
+++ b/apps/dav/tests/unit/ServerTest.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit;
+
+use OCA\DAV\Server;
+use OCP\IRequest;
+
+/**
+ * Class ServerTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit
+ */
+class ServerTest extends \Test\TestCase {
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesUris')]
+ public function test(string $uri, array $plugins): void {
+ /** @var IRequest | \PHPUnit\Framework\MockObject\MockObject $r */
+ $r = $this->createMock(IRequest::class);
+ $r->expects($this->any())->method('getRequestUri')->willReturn($uri);
+ $this->loginAsUser('admin');
+ $s = new Server($r, '/');
+ $this->assertNotNull($s->server);
+ foreach ($plugins as $plugin) {
+ $this->assertNotNull($s->server->getPlugin($plugin));
+ }
+ }
+ public static function providesUris(): array {
+ return [
+ 'principals' => ['principals/users/admin', ['caldav', 'oc-resource-sharing', 'carddav']],
+ 'calendars' => ['calendars/admin', ['caldav', 'oc-resource-sharing']],
+ 'addressbooks' => ['addressbooks/admin', ['carddav', 'oc-resource-sharing']],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/Service/AbsenceServiceTest.php b/apps/dav/tests/unit/Service/AbsenceServiceTest.php
new file mode 100644
index 00000000000..c16c715d5c2
--- /dev/null
+++ b/apps/dav/tests/unit/Service/AbsenceServiceTest.php
@@ -0,0 +1,445 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Service;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Db\Absence;
+use OCA\DAV\Db\AbsenceMapper;
+use OCA\DAV\Service\AbsenceService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUser;
+use OCP\User\Events\OutOfOfficeChangedEvent;
+use OCP\User\Events\OutOfOfficeScheduledEvent;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class AbsenceServiceTest extends TestCase {
+ private AbsenceService $absenceService;
+ private AbsenceMapper&MockObject $absenceMapper;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private IJobList&MockObject $jobList;
+ private TimezoneService&MockObject $timezoneService;
+ private ITimeFactory&MockObject $timeFactory;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->absenceMapper = $this->createMock(AbsenceMapper::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $this->jobList = $this->createMock(IJobList::class);
+ $this->timezoneService = $this->createMock(TimezoneService::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+
+ $this->absenceService = new AbsenceService(
+ $this->absenceMapper,
+ $this->eventDispatcher,
+ $this->jobList,
+ $this->timezoneService,
+ $this->timeFactory,
+ );
+ }
+
+ public function testCreateAbsenceEmitsScheduledEvent(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willThrowException(new DoesNotExistException('foo bar'));
+ $this->absenceMapper->expects(self::once())
+ ->method('insert')
+ ->willReturnCallback(function (Absence $absence): Absence {
+ $absence->setId(1);
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn('Europe/Berlin');
+ $this->eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::callback(static function (Event $event) use ($user, $tz): bool {
+ self::assertInstanceOf(OutOfOfficeScheduledEvent::class, $event);
+ /** @var OutOfOfficeScheduledEvent $event */
+ $data = $event->getData();
+ self::assertEquals('1', $data->getId());
+ self::assertEquals($user, $data->getUser());
+ self::assertEquals(
+ (new DateTimeImmutable('2023-01-05', $tz))->getTimeStamp(),
+ $data->getStartDate(),
+ );
+ self::assertEquals(
+ (new DateTimeImmutable('2023-01-10', $tz))->getTimeStamp() + 3600 * 23 + 59 * 60,
+ $data->getEndDate(),
+ );
+ self::assertEquals('status', $data->getShortMessage());
+ self::assertEquals('message', $data->getMessage());
+ return true;
+ }));
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn(PHP_INT_MAX);
+ $this->jobList->expects(self::never())
+ ->method('scheduleAfter');
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ '2023-01-10',
+ 'status',
+ 'message',
+ );
+ }
+
+ public function testUpdateAbsenceEmitsChangedEvent(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+ $absence = new Absence();
+ $absence->setId(1);
+ $absence->setFirstDay('1970-01-01');
+ $absence->setLastDay('1970-01-10');
+ $absence->setStatus('old status');
+ $absence->setMessage('old message');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willReturn($absence);
+ $this->absenceMapper->expects(self::once())
+ ->method('update')
+ ->willReturnCallback(static function (Absence $absence): Absence {
+ self::assertEquals('2023-01-05', $absence->getFirstDay());
+ self::assertEquals('2023-01-10', $absence->getLastDay());
+ self::assertEquals('status', $absence->getStatus());
+ self::assertEquals('message', $absence->getMessage());
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn('Europe/Berlin');
+ $this->eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::callback(static function (Event $event) use ($user, $tz): bool {
+ self::assertInstanceOf(OutOfOfficeChangedEvent::class, $event);
+ /** @var OutOfOfficeChangedEvent $event */
+ $data = $event->getData();
+ self::assertEquals('1', $data->getId());
+ self::assertEquals($user, $data->getUser());
+ self::assertEquals(
+ (new DateTimeImmutable('2023-01-05', $tz))->getTimeStamp(),
+ $data->getStartDate(),
+ );
+ self::assertEquals(
+ (new DateTimeImmutable('2023-01-10', $tz))->getTimeStamp() + 3600 * 23 + 59 * 60,
+ $data->getEndDate(),
+ );
+ self::assertEquals('status', $data->getShortMessage());
+ self::assertEquals('message', $data->getMessage());
+ return true;
+ }));
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn(PHP_INT_MAX);
+ $this->jobList->expects(self::never())
+ ->method('scheduleAfter');
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ '2023-01-10',
+ 'status',
+ 'message',
+ );
+ }
+
+ public function testCreateAbsenceSchedulesBothJobs(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $startDateString = '2023-01-05';
+ $startDate = new DateTimeImmutable($startDateString, $tz);
+ $endDateString = '2023-01-10';
+ $endDate = new DateTimeImmutable($endDateString, $tz);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willThrowException(new DoesNotExistException('foo bar'));
+ $this->absenceMapper->expects(self::once())
+ ->method('insert')
+ ->willReturnCallback(function (Absence $absence): Absence {
+ $absence->setId(1);
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-01', $tz))->getTimestamp());
+ $this->jobList->expects(self::exactly(2))
+ ->method('scheduleAfter')
+ ->willReturnMap([
+ [OutOfOfficeEventDispatcherJob::class, $startDate->getTimestamp(), [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+ ]],
+ [OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 3600 * 23 + 59 * 60, [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ]],
+ ]);
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ $startDateString,
+ $endDateString,
+ '',
+ '',
+ );
+ }
+
+ public function testCreateAbsenceSchedulesOnlyEndJob(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $endDateString = '2023-01-10';
+ $endDate = new DateTimeImmutable($endDateString, $tz);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willThrowException(new DoesNotExistException('foo bar'));
+ $this->absenceMapper->expects(self::once())
+ ->method('insert')
+ ->willReturnCallback(function (Absence $absence): Absence {
+ $absence->setId(1);
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-07', $tz))->getTimestamp());
+ $this->jobList->expects(self::once())
+ ->method('scheduleAfter')
+ ->with(OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 3600 * 23 + 59 * 60, [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ]);
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ $endDateString,
+ '',
+ '',
+ );
+ }
+
+ public function testCreateAbsenceSchedulesNoJob(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willThrowException(new DoesNotExistException('foo bar'));
+ $this->absenceMapper->expects(self::once())
+ ->method('insert')
+ ->willReturnCallback(function (Absence $absence): Absence {
+ $absence->setId(1);
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-12', $tz))->getTimestamp());
+ $this->jobList->expects(self::never())
+ ->method('scheduleAfter');
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ '2023-01-10',
+ '',
+ '',
+ );
+ }
+
+ public function testUpdateAbsenceSchedulesBothJobs(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $startDateString = '2023-01-05';
+ $startDate = new DateTimeImmutable($startDateString, $tz);
+ $endDateString = '2023-01-10';
+ $endDate = new DateTimeImmutable($endDateString, $tz);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+ $absence = new Absence();
+ $absence->setId(1);
+ $absence->setFirstDay('1970-01-01');
+ $absence->setLastDay('1970-01-10');
+ $absence->setStatus('old status');
+ $absence->setMessage('old message');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willReturn($absence);
+ $this->absenceMapper->expects(self::once())
+ ->method('update')
+ ->willReturnCallback(static function (Absence $absence) use ($startDateString, $endDateString): Absence {
+ self::assertEquals($startDateString, $absence->getFirstDay());
+ self::assertEquals($endDateString, $absence->getLastDay());
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-01', $tz))->getTimestamp());
+ $this->jobList->expects(self::exactly(2))
+ ->method('scheduleAfter')
+ ->willReturnMap([
+ [OutOfOfficeEventDispatcherJob::class, $startDate->getTimestamp(), [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+ ]],
+ [OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 3600 * 23 + 59 * 60, [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ]],
+ ]);
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ $startDateString,
+ $endDateString,
+ '',
+ '',
+ );
+ }
+
+ public function testUpdateSchedulesOnlyEndJob(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $endDateString = '2023-01-10';
+ $endDate = new DateTimeImmutable($endDateString, $tz);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+ $absence = new Absence();
+ $absence->setId(1);
+ $absence->setFirstDay('1970-01-01');
+ $absence->setLastDay('1970-01-10');
+ $absence->setStatus('old status');
+ $absence->setMessage('old message');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willReturn($absence);
+ $this->absenceMapper->expects(self::once())
+ ->method('update')
+ ->willReturnCallback(static function (Absence $absence) use ($endDateString): Absence {
+ self::assertEquals('2023-01-05', $absence->getFirstDay());
+ self::assertEquals($endDateString, $absence->getLastDay());
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-07', $tz))->getTimestamp());
+ $this->jobList->expects(self::once())
+ ->method('scheduleAfter')
+ ->with(OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 23 * 3600 + 59 * 60, [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ]);
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ $endDateString,
+ '',
+ '',
+ );
+ }
+
+ public function testUpdateAbsenceSchedulesNoJob(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+ $absence = new Absence();
+ $absence->setId(1);
+ $absence->setFirstDay('1970-01-01');
+ $absence->setLastDay('1970-01-10');
+ $absence->setStatus('old status');
+ $absence->setMessage('old message');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willReturn($absence);
+ $this->absenceMapper->expects(self::once())
+ ->method('update')
+ ->willReturnCallback(static function (Absence $absence): Absence {
+ self::assertEquals('2023-01-05', $absence->getFirstDay());
+ self::assertEquals('2023-01-10', $absence->getLastDay());
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-12', $tz))->getTimestamp());
+ $this->jobList->expects(self::never())
+ ->method('scheduleAfter');
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ '2023-01-10',
+ '',
+ '',
+ );
+ }
+}
diff --git a/apps/dav/tests/unit/Service/ExampleContactServiceTest.php b/apps/dav/tests/unit/Service/ExampleContactServiceTest.php
new file mode 100644
index 00000000000..027b66a6fb2
--- /dev/null
+++ b/apps/dav/tests/unit/Service/ExampleContactServiceTest.php
@@ -0,0 +1,194 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Service;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\Service\ExampleContactService;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Services\IAppConfig;
+use OCP\Files\AppData\IAppDataFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Uid\Uuid;
+use Test\TestCase;
+
+class ExampleContactServiceTest extends TestCase {
+ protected ExampleContactService $service;
+ protected CardDavBackend&MockObject $cardDav;
+ protected IAppManager&MockObject $appManager;
+ protected IAppDataFactory&MockObject $appDataFactory;
+ protected LoggerInterface&MockObject $logger;
+ protected IAppConfig&MockObject $appConfig;
+ protected IAppData&MockObject $appData;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->cardDav = $this->createMock(CardDavBackend::class);
+ $this->appDataFactory = $this->createMock(IAppDataFactory::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->appConfig = $this->createMock(IAppConfig::class);
+
+ $this->appData = $this->createMock(IAppData::class);
+ $this->appDataFactory->method('get')
+ ->with('dav')
+ ->willReturn($this->appData);
+
+ $this->service = new ExampleContactService(
+ $this->appDataFactory,
+ $this->appConfig,
+ $this->logger,
+ $this->cardDav,
+ );
+ }
+
+ public function testCreateDefaultContactWithInvalidCard(): void {
+ // Invalid vCard missing required FN property
+ $vcardContent = "BEGIN:VCARD\nVERSION:3.0\nEND:VCARD";
+ $this->appConfig->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn(true);
+ $folder = $this->createMock(ISimpleFolder::class);
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getContent')->willReturn($vcardContent);
+ $folder->method('getFile')->willReturn($file);
+ $this->appData->method('getFolder')->willReturn($folder);
+
+ $this->logger->expects($this->once())
+ ->method('error')
+ ->with('Default contact is invalid', $this->anything());
+
+ $this->cardDav->expects($this->never())
+ ->method('createCard');
+
+ $this->service->createDefaultContact(123);
+ }
+
+ public function testUidAndRevAreUpdated(): void {
+ $originalUid = 'original-uid';
+ $originalRev = '20200101T000000Z';
+ $vcardContent = "BEGIN:VCARD\nVERSION:3.0\nFN:Test User\nUID:$originalUid\nREV:$originalRev\nEND:VCARD";
+
+ $this->appConfig->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn(true);
+ $folder = $this->createMock(ISimpleFolder::class);
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getContent')->willReturn($vcardContent);
+ $folder->method('getFile')->willReturn($file);
+ $this->appData->method('getFolder')->willReturn($folder);
+
+ $capturedCardData = null;
+ $this->cardDav->expects($this->once())
+ ->method('createCard')
+ ->with(
+ $this->anything(),
+ $this->anything(),
+ $this->callback(function ($cardData) use (&$capturedCardData) {
+ $capturedCardData = $cardData;
+ return true;
+ }),
+ $this->anything()
+ )->willReturn(null);
+
+ $this->service->createDefaultContact(123);
+
+ $vcard = \Sabre\VObject\Reader::read($capturedCardData);
+ $this->assertNotEquals($originalUid, $vcard->UID->getValue());
+ $this->assertTrue(Uuid::isValid($vcard->UID->getValue()));
+ $this->assertNotEquals($originalRev, $vcard->REV->getValue());
+ }
+
+ public function testDefaultContactFileDoesNotExist(): void {
+ $this->appConfig->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn(true);
+ $this->appData->method('getFolder')->willThrowException(new NotFoundException());
+
+ $this->cardDav->expects($this->never())
+ ->method('createCard');
+
+ $this->service->createDefaultContact(123);
+ }
+
+ public function testUidAndRevAreAddedIfMissing(): void {
+ $vcardContent = "BEGIN:VCARD\nVERSION:3.0\nFN:Test User\nEND:VCARD";
+
+ $this->appConfig->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn(true);
+ $folder = $this->createMock(ISimpleFolder::class);
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getContent')->willReturn($vcardContent);
+ $folder->method('getFile')->willReturn($file);
+ $this->appData->method('getFolder')->willReturn($folder);
+
+ $capturedCardData = 'new-card-data';
+
+ $this->cardDav
+ ->expects($this->once())
+ ->method('createCard')
+ ->with(
+ $this->anything(),
+ $this->anything(),
+ $this->callback(function ($cardData) use (&$capturedCardData) {
+ $capturedCardData = $cardData;
+ return true;
+ }),
+ $this->anything()
+ );
+
+ $this->service->createDefaultContact(123);
+ $vcard = \Sabre\VObject\Reader::read($capturedCardData);
+
+ $this->assertNotNull($vcard->REV);
+ $this->assertNotNull($vcard->UID);
+ $this->assertTrue(Uuid::isValid($vcard->UID->getValue()));
+ }
+
+ public function testDefaultContactIsNotCreatedIfEnabled(): void {
+ $this->appConfig->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn(false);
+ $this->logger->expects($this->never())
+ ->method('error');
+ $this->cardDav->expects($this->never())
+ ->method('createCard');
+
+ $this->service->createDefaultContact(123);
+ }
+
+ public static function provideDefaultContactEnableData(): array {
+ return [[true], [false]];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('provideDefaultContactEnableData')]
+ public function testIsDefaultContactEnabled(bool $enabled): void {
+ $this->appConfig->expects(self::once())
+ ->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn($enabled);
+
+ $this->assertEquals($enabled, $this->service->isDefaultContactEnabled());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('provideDefaultContactEnableData')]
+ public function testSetDefaultContactEnabled(bool $enabled): void {
+ $this->appConfig->expects(self::once())
+ ->method('setAppValueBool')
+ ->with('enableDefaultContact', $enabled);
+
+ $this->service->setDefaultContactEnabled($enabled);
+ }
+}
diff --git a/apps/dav/tests/unit/Service/ExampleEventServiceTest.php b/apps/dav/tests/unit/Service/ExampleEventServiceTest.php
new file mode 100644
index 00000000000..0f423624fb8
--- /dev/null
+++ b/apps/dav/tests/unit/Service/ExampleEventServiceTest.php
@@ -0,0 +1,196 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Service;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Service\ExampleEventService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\IAppConfig;
+use OCP\IL10N;
+use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class ExampleEventServiceTest extends TestCase {
+ private ExampleEventService $service;
+
+ private CalDavBackend&MockObject $calDavBackend;
+ private ISecureRandom&MockObject $random;
+ private ITimeFactory&MockObject $time;
+ private IAppData&MockObject $appData;
+ private IAppConfig&MockObject $appConfig;
+ private IL10N&MockObject $l10n;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calDavBackend = $this->createMock(CalDavBackend::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $this->time = $this->createMock(ITimeFactory::class);
+ $this->appData = $this->createMock(IAppData::class);
+ $this->appConfig = $this->createMock(IAppConfig::class);
+ $this->l10n = $this->createMock(IL10N::class);
+
+ $this->l10n->method('t')
+ ->willReturnArgument(0);
+
+ $this->service = new ExampleEventService(
+ $this->calDavBackend,
+ $this->random,
+ $this->time,
+ $this->appData,
+ $this->appConfig,
+ $this->l10n,
+ );
+ }
+
+ public static function provideCustomEventData(): array {
+ return [
+ [file_get_contents(__DIR__ . '/../test_fixtures/example-event.ics')],
+ [file_get_contents(__DIR__ . '/../test_fixtures/example-event-with-attendees.ics')],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('provideCustomEventData')]
+ public function testCreateExampleEventWithCustomEvent($customEventIcs): void {
+ $this->appConfig->expects(self::once())
+ ->method('getValueBool')
+ ->with('dav', 'create_example_event', true)
+ ->willReturn(true);
+
+ $exampleEventFolder = $this->createMock(ISimpleFolder::class);
+ $this->appData->expects(self::once())
+ ->method('getFolder')
+ ->with('example_event')
+ ->willReturn($exampleEventFolder);
+ $exampleEventFile = $this->createMock(ISimpleFile::class);
+ $exampleEventFolder->expects(self::once())
+ ->method('getFile')
+ ->with('example_event.ics')
+ ->willReturn($exampleEventFile);
+ $exampleEventFile->expects(self::once())
+ ->method('getContent')
+ ->willReturn($customEventIcs);
+
+ $this->random->expects(self::once())
+ ->method('generate')
+ ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
+ ->willReturn('RANDOM-UID');
+
+ $now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
+ $this->time->expects(self::exactly(2))
+ ->method('now')
+ ->willReturn($now);
+
+ $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-expected.ics');
+ $this->calDavBackend->expects(self::once())
+ ->method('createCalendarObject')
+ ->with(1000, 'RANDOM-UID.ics', $expectedIcs);
+
+ $this->service->createExampleEvent(1000);
+ }
+
+ public function testCreateExampleEventWithDefaultEvent(): void {
+ $this->appConfig->expects(self::once())
+ ->method('getValueBool')
+ ->with('dav', 'create_example_event', true)
+ ->willReturn(true);
+
+ $this->appData->expects(self::once())
+ ->method('getFolder')
+ ->with('example_event')
+ ->willThrowException(new NotFoundException());
+
+ $this->random->expects(self::once())
+ ->method('generate')
+ ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
+ ->willReturn('RANDOM-UID');
+
+ $now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
+ $this->time->expects(self::exactly(3))
+ ->method('now')
+ ->willReturn($now);
+
+ $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-default-expected.ics');
+ $this->calDavBackend->expects(self::once())
+ ->method('createCalendarObject')
+ ->with(1000, 'RANDOM-UID.ics', $expectedIcs);
+
+ $this->service->createExampleEvent(1000);
+ }
+
+ public function testCreateExampleWhenDisabled(): void {
+ $this->appConfig->expects(self::once())
+ ->method('getValueBool')
+ ->with('dav', 'create_example_event', true)
+ ->willReturn(false);
+
+ $this->calDavBackend->expects(self::never())
+ ->method('createCalendarObject');
+
+ $this->service->createExampleEvent(1000);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('provideCustomEventData')]
+ public function testGetExampleEventWithCustomEvent($customEventIcs): void {
+ $exampleEventFolder = $this->createMock(ISimpleFolder::class);
+ $this->appData->expects(self::once())
+ ->method('getFolder')
+ ->with('example_event')
+ ->willReturn($exampleEventFolder);
+ $exampleEventFile = $this->createMock(ISimpleFile::class);
+ $exampleEventFolder->expects(self::once())
+ ->method('getFile')
+ ->with('example_event.ics')
+ ->willReturn($exampleEventFile);
+ $exampleEventFile->expects(self::once())
+ ->method('getContent')
+ ->willReturn($customEventIcs);
+
+ $this->random->expects(self::once())
+ ->method('generate')
+ ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
+ ->willReturn('RANDOM-UID');
+
+ $now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
+ $this->time->expects(self::exactly(2))
+ ->method('now')
+ ->willReturn($now);
+
+ $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-expected.ics');
+ $actualIcs = $this->service->getExampleEvent()->getIcs();
+ $this->assertEquals($expectedIcs, $actualIcs);
+ }
+
+ public function testGetExampleEventWithDefault(): void {
+ $this->appData->expects(self::once())
+ ->method('getFolder')
+ ->with('example_event')
+ ->willThrowException(new NotFoundException());
+
+ $this->random->expects(self::once())
+ ->method('generate')
+ ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
+ ->willReturn('RANDOM-UID');
+
+ $now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
+ $this->time->expects(self::exactly(3))
+ ->method('now')
+ ->willReturn($now);
+
+ $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-default-expected.ics');
+ $actualIcs = $this->service->getExampleEvent()->getIcs();
+ $this->assertEquals($expectedIcs, $actualIcs);
+ }
+}
diff --git a/apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php b/apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php
new file mode 100644
index 00000000000..fdfe37d8918
--- /dev/null
+++ b/apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php
@@ -0,0 +1,89 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\DAV\Service;
+
+use DateTimeImmutable;
+use OCA\DAV\CalDAV\UpcomingEventsService;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Calendar\ICalendarQuery;
+use OCP\Calendar\IManager;
+use OCP\IURLGenerator;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class UpcomingEventsServiceTest extends TestCase {
+
+ private IManager&MockObject $calendarManager;
+ private ITimeFactory&MockObject $timeFactory;
+ private IUserManager&MockObject $userManager;
+ private IAppManager&MockObject $appManager;
+ private IURLGenerator&MockObject $urlGenerator;
+ private UpcomingEventsService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calendarManager = $this->createMock(IManager::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+
+ $this->service = new UpcomingEventsService(
+ $this->calendarManager,
+ $this->timeFactory,
+ $this->userManager,
+ $this->appManager,
+ $this->urlGenerator,
+ );
+ }
+
+ public function testGetEventsByLocation(): void {
+ $now = new DateTimeImmutable('2024-07-08T18:20:20Z');
+ $this->timeFactory->method('now')
+ ->willReturn($now);
+ $query = $this->createMock(ICalendarQuery::class);
+ $this->appManager->method('isEnabledForUser')->willReturn(false);
+ $this->calendarManager->method('newQuery')
+ ->with('principals/users/user1')
+ ->willReturn($query);
+ $query->expects(self::once())
+ ->method('addSearchProperty')
+ ->with('LOCATION');
+ $query->expects(self::once())
+ ->method('setSearchPattern')
+ ->with('https://cloud.example.com/call/123');
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->with($query)
+ ->willReturn([
+ [
+ 'uri' => 'ev1',
+ 'calendar-key' => '1',
+ 'calendar-uri' => 'personal',
+ 'objects' => [
+ 0 => [
+ 'DTSTART' => [
+ new DateTimeImmutable('now'),
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $events = $this->service->getEvents('user1', 'https://cloud.example.com/call/123');
+
+ self::assertCount(1, $events);
+ $event1 = $events[0];
+ self::assertEquals('ev1', $event1->getUri());
+ }
+}
diff --git a/apps/dav/tests/unit/Settings/CalDAVSettingsTest.php b/apps/dav/tests/unit/Settings/CalDAVSettingsTest.php
new file mode 100644
index 00000000000..032759d64b7
--- /dev/null
+++ b/apps/dav/tests/unit/Settings/CalDAVSettingsTest.php
@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV\Settings;
+
+use OCA\DAV\Settings\CalDAVSettings;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
+use OCP\IConfig;
+use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class CalDAVSettingsTest extends TestCase {
+ private IConfig&MockObject $config;
+ private IInitialState&MockObject $initialState;
+ private IURLGenerator&MockObject $urlGenerator;
+ private IAppManager&MockObject $appManager;
+ private CalDAVSettings $settings;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->initialState = $this->createMock(IInitialState::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->settings = new CalDAVSettings($this->config, $this->initialState, $this->urlGenerator, $this->appManager);
+ }
+
+ public function testGetForm(): void {
+ $this->config->method('getAppValue')
+ ->willReturnMap([
+ ['dav', 'sendInvitations', 'yes', 'yes'],
+ ['dav', 'generateBirthdayCalendar', 'yes', 'no'],
+ ['dav', 'sendEventReminders', 'yes', 'yes'],
+ ['dav', 'sendEventRemindersToSharedUsers', 'yes', 'yes'],
+ ['dav', 'sendEventRemindersPush', 'yes', 'yes'],
+ ]);
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToDocs')
+ ->with('user-sync-calendars')
+ ->willReturn('Some docs URL');
+
+ $calls = [
+ ['userSyncCalendarsDocUrl', 'Some docs URL'],
+ ['sendInvitations', true],
+ ['generateBirthdayCalendar', false],
+ ['sendEventReminders', true],
+ ['sendEventRemindersToSharedUsers', true],
+ ['sendEventRemindersPush', true],
+ ];
+ $this->initialState->method('provideInitialState')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+ $result = $this->settings->getForm();
+
+ $this->assertInstanceOf(TemplateResponse::class, $result);
+ }
+
+ public function testGetSection(): void {
+ $this->appManager->expects(self::once())
+ ->method('isBackendRequired')
+ ->with(IAppManager::BACKEND_CALDAV)
+ ->willReturn(true);
+ $this->assertEquals('groupware', $this->settings->getSection());
+ }
+
+ public function testGetSectionWithoutCaldavBackend(): void {
+ $this->appManager->expects(self::once())
+ ->method('isBackendRequired')
+ ->with(IAppManager::BACKEND_CALDAV)
+ ->willReturn(false);
+ $this->assertEquals(null, $this->settings->getSection());
+ }
+
+ public function testGetPriority(): void {
+ $this->assertEquals(10, $this->settings->getPriority());
+ }
+}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php b/apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php
new file mode 100644
index 00000000000..39342811377
--- /dev/null
+++ b/apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php
@@ -0,0 +1,157 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\SystemTag;
+
+use OC\SystemTag\SystemTag;
+use OCA\DAV\SystemTag\SystemTagMappingNode;
+use OCP\IUser;
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
+use OCP\SystemTag\TagNotFoundException;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class SystemTagMappingNodeTest extends \Test\TestCase {
+ private ISystemTagManager&MockObject $tagManager;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private IUser&MockObject $user;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ $this->user = $this->createMock(IUser::class);
+ }
+
+ public function getMappingNode($tag = null, array $writableNodeIds = []) {
+ if ($tag === null) {
+ $tag = new SystemTag('1', 'Test', true, true);
+ }
+ return new SystemTagMappingNode(
+ $tag,
+ '123',
+ 'files',
+ $this->user,
+ $this->tagManager,
+ $this->tagMapper,
+ fn ($id): bool => in_array($id, $writableNodeIds),
+ );
+ }
+
+ public function testGetters(): void {
+ $tag = new SystemTag('1', 'Test', true, false);
+ $node = $this->getMappingNode($tag);
+ $this->assertEquals('1', $node->getName());
+ $this->assertEquals($tag, $node->getSystemTag());
+ $this->assertEquals(123, $node->getObjectId());
+ $this->assertEquals('files', $node->getObjectType());
+ }
+
+ public function testDeleteTag(): void {
+ $node = $this->getMappingNode(null, [123]);
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($node->getSystemTag())
+ ->willReturn(true);
+ $this->tagManager->expects($this->once())
+ ->method('canUserAssignTag')
+ ->with($node->getSystemTag())
+ ->willReturn(true);
+ $this->tagManager->expects($this->never())
+ ->method('deleteTags');
+ $this->tagMapper->expects($this->once())
+ ->method('unassignTags')
+ ->with(123, 'files', 1);
+
+ $node->delete();
+ }
+
+ public function testDeleteTagForbidden(): void {
+ $node = $this->getMappingNode();
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($node->getSystemTag())
+ ->willReturn(true);
+ $this->tagManager->expects($this->once())
+ ->method('canUserAssignTag')
+ ->with($node->getSystemTag())
+ ->willReturn(true);
+ $this->tagManager->expects($this->never())
+ ->method('deleteTags');
+ $this->tagMapper->expects($this->never())
+ ->method('unassignTags');
+
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $node->delete();
+ }
+
+ public static function tagNodeDeleteProviderPermissionException(): array {
+ return [
+ [
+ // cannot unassign invisible tag
+ new SystemTag('1', 'Original', false, true),
+ 'Sabre\DAV\Exception\NotFound',
+ ],
+ [
+ // cannot unassign non-assignable tag
+ new SystemTag('1', 'Original', true, false),
+ 'Sabre\DAV\Exception\Forbidden',
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeDeleteProviderPermissionException')]
+ public function testDeleteTagExpectedException(ISystemTag $tag, $expectedException): void {
+ $this->tagManager->expects($this->any())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn($tag->isUserVisible());
+ $this->tagManager->expects($this->any())
+ ->method('canUserAssignTag')
+ ->with($tag)
+ ->willReturn($tag->isUserAssignable());
+ $this->tagManager->expects($this->never())
+ ->method('deleteTags');
+ $this->tagMapper->expects($this->never())
+ ->method('unassignTags');
+
+ $thrown = null;
+ try {
+ $this->getMappingNode($tag)->delete();
+ } catch (\Exception $e) {
+ $thrown = $e;
+ }
+
+ $this->assertInstanceOf($expectedException, $thrown);
+ }
+
+
+ public function testDeleteTagNotFound(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ // assuming the tag existed at the time the node was created,
+ // but got deleted concurrently in the database
+ $tag = new SystemTag('1', 'Test', true, true);
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn($tag->isUserVisible());
+ $this->tagManager->expects($this->once())
+ ->method('canUserAssignTag')
+ ->with($tag)
+ ->willReturn($tag->isUserAssignable());
+ $this->tagMapper->expects($this->once())
+ ->method('unassignTags')
+ ->with(123, 'files', 1)
+ ->willThrowException(new TagNotFoundException());
+
+ $this->getMappingNode($tag, [123])->delete();
+ }
+}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php b/apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php
new file mode 100644
index 00000000000..594b5e15db6
--- /dev/null
+++ b/apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php
@@ -0,0 +1,272 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\SystemTag;
+
+use OC\SystemTag\SystemTag;
+use OCA\DAV\SystemTag\SystemTagNode;
+use OCP\IUser;
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
+use OCP\SystemTag\TagAlreadyExistsException;
+use OCP\SystemTag\TagNotFoundException;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\Forbidden;
+
+class SystemTagNodeTest extends \Test\TestCase {
+ private ISystemTagManager&MockObject $tagManager;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private IUser&MockObject $user;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ $this->user = $this->createMock(IUser::class);
+ }
+
+ protected function getTagNode($isAdmin = true, $tag = null) {
+ if ($tag === null) {
+ $tag = new SystemTag('1', 'Test', true, true);
+ }
+ return new SystemTagNode(
+ $tag,
+ $this->user,
+ $isAdmin,
+ $this->tagManager,
+ $this->tagMapper,
+ );
+ }
+
+ public static function adminFlagProvider(): array {
+ return [[true], [false]];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('adminFlagProvider')]
+ public function testGetters(bool $isAdmin): void {
+ $tag = new SystemTag('1', 'Test', true, true);
+ $node = $this->getTagNode($isAdmin, $tag);
+ $this->assertEquals('1', $node->getName());
+ $this->assertEquals($tag, $node->getSystemTag());
+ }
+
+
+ public function testSetName(): void {
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+
+ $this->getTagNode()->setName('2');
+ }
+
+ public static function tagNodeProvider(): array {
+ return [
+ // admin
+ [
+ true,
+ new SystemTag('1', 'Original', true, true),
+ ['Renamed', true, true, null]
+ ],
+ [
+ true,
+ new SystemTag('1', 'Original', true, true),
+ ['Original', false, false, null]
+ ],
+ // non-admin
+ [
+ // renaming allowed
+ false,
+ new SystemTag('1', 'Original', true, true),
+ ['Rename', true, true, '0082c9']
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeProvider')]
+ public function testUpdateTag(bool $isAdmin, ISystemTag $originalTag, array $changedArgs): void {
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($originalTag)
+ ->willReturn($originalTag->isUserVisible() || $isAdmin);
+ $this->tagManager->expects($this->once())
+ ->method('canUserAssignTag')
+ ->with($originalTag)
+ ->willReturn($originalTag->isUserAssignable() || $isAdmin);
+ $this->tagManager->expects($this->once())
+ ->method('updateTag')
+ ->with(1, $changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]);
+ $this->getTagNode($isAdmin, $originalTag)
+ ->update($changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]);
+ }
+
+ public static function tagNodeProviderPermissionException(): array {
+ return [
+ [
+ // changing permissions not allowed
+ new SystemTag('1', 'Original', true, true),
+ ['Original', false, true, ''],
+ 'Sabre\DAV\Exception\Forbidden',
+ ],
+ [
+ // changing permissions not allowed
+ new SystemTag('1', 'Original', true, true),
+ ['Original', true, false, ''],
+ 'Sabre\DAV\Exception\Forbidden',
+ ],
+ [
+ // changing permissions not allowed
+ new SystemTag('1', 'Original', true, true),
+ ['Original', false, false, ''],
+ 'Sabre\DAV\Exception\Forbidden',
+ ],
+ [
+ // changing non-assignable not allowed
+ new SystemTag('1', 'Original', true, false),
+ ['Rename', true, false, ''],
+ 'Sabre\DAV\Exception\Forbidden',
+ ],
+ [
+ // changing non-assignable not allowed
+ new SystemTag('1', 'Original', true, false),
+ ['Original', true, true, ''],
+ 'Sabre\DAV\Exception\Forbidden',
+ ],
+ [
+ // invisible tag does not exist
+ new SystemTag('1', 'Original', false, false),
+ ['Rename', false, false, ''],
+ 'Sabre\DAV\Exception\NotFound',
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeProviderPermissionException')]
+ public function testUpdateTagPermissionException(ISystemTag $originalTag, array $changedArgs, string $expectedException): void {
+ $this->tagManager->expects($this->any())
+ ->method('canUserSeeTag')
+ ->with($originalTag)
+ ->willReturn($originalTag->isUserVisible());
+ $this->tagManager->expects($this->any())
+ ->method('canUserAssignTag')
+ ->with($originalTag)
+ ->willReturn($originalTag->isUserAssignable());
+ $this->tagManager->expects($this->never())
+ ->method('updateTag');
+
+ $thrown = null;
+
+ try {
+ $this->getTagNode(false, $originalTag)
+ ->update($changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]);
+ } catch (\Exception $e) {
+ $thrown = $e;
+ }
+
+ $this->assertInstanceOf($expectedException, $thrown);
+ }
+
+
+ public function testUpdateTagAlreadyExists(): void {
+ $this->expectException(\Sabre\DAV\Exception\Conflict::class);
+
+ $tag = new SystemTag('1', 'tag1', true, true);
+ $this->tagManager->expects($this->any())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn(true);
+ $this->tagManager->expects($this->any())
+ ->method('canUserAssignTag')
+ ->with($tag)
+ ->willReturn(true);
+ $this->tagManager->expects($this->once())
+ ->method('updateTag')
+ ->with(1, 'Renamed', true, true)
+ ->willThrowException(new TagAlreadyExistsException());
+ $this->getTagNode(false, $tag)->update('Renamed', true, true, null);
+ }
+
+
+ public function testUpdateTagNotFound(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $tag = new SystemTag('1', 'tag1', true, true);
+ $this->tagManager->expects($this->any())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn(true);
+ $this->tagManager->expects($this->any())
+ ->method('canUserAssignTag')
+ ->with($tag)
+ ->willReturn(true);
+ $this->tagManager->expects($this->once())
+ ->method('updateTag')
+ ->with(1, 'Renamed', true, true)
+ ->willThrowException(new TagNotFoundException());
+ $this->getTagNode(false, $tag)->update('Renamed', true, true, null);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('adminFlagProvider')]
+ public function testDeleteTag(bool $isAdmin): void {
+ $tag = new SystemTag('1', 'tag1', true, true);
+ $this->tagManager->expects($isAdmin ? $this->once() : $this->never())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn(true);
+ $this->tagManager->expects($isAdmin ? $this->once() : $this->never())
+ ->method('deleteTags')
+ ->with('1');
+ if (!$isAdmin) {
+ $this->expectException(Forbidden::class);
+ }
+ $this->getTagNode($isAdmin, $tag)->delete();
+ }
+
+ public static function tagNodeDeleteProviderPermissionException(): array {
+ return [
+ [
+ // cannot delete invisible tag
+ new SystemTag('1', 'Original', false, true),
+ 'Sabre\DAV\Exception\Forbidden',
+ ],
+ [
+ // cannot delete non-assignable tag
+ new SystemTag('1', 'Original', true, false),
+ 'Sabre\DAV\Exception\Forbidden',
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeDeleteProviderPermissionException')]
+ public function testDeleteTagPermissionException(ISystemTag $tag, string $expectedException): void {
+ $this->tagManager->expects($this->any())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn($tag->isUserVisible());
+ $this->tagManager->expects($this->never())
+ ->method('deleteTags');
+
+ $this->expectException($expectedException);
+ $this->getTagNode(false, $tag)->delete();
+ }
+
+
+ public function testDeleteTagNotFound(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $tag = new SystemTag('1', 'tag1', true, true);
+ $this->tagManager->expects($this->any())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn($tag->isUserVisible());
+ $this->tagManager->expects($this->once())
+ ->method('deleteTags')
+ ->with('1')
+ ->willThrowException(new TagNotFoundException());
+ $this->getTagNode(true, $tag)->delete();
+ }
+}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php b/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php
new file mode 100644
index 00000000000..e0c4685c1fb
--- /dev/null
+++ b/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php
@@ -0,0 +1,664 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\SystemTag;
+
+use OC\SystemTag\SystemTag;
+use OCA\DAV\SystemTag\SystemTagNode;
+use OCA\DAV\SystemTag\SystemTagPlugin;
+use OCA\DAV\SystemTag\SystemTagsByIdCollection;
+use OCA\DAV\SystemTag\SystemTagsObjectMappingCollection;
+use OCP\Files\IRootFolder;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
+use OCP\SystemTag\TagAlreadyExistsException;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+class SystemTagPluginTest extends \Test\TestCase {
+ public const ID_PROPERTYNAME = SystemTagPlugin::ID_PROPERTYNAME;
+ public const DISPLAYNAME_PROPERTYNAME = SystemTagPlugin::DISPLAYNAME_PROPERTYNAME;
+ public const USERVISIBLE_PROPERTYNAME = SystemTagPlugin::USERVISIBLE_PROPERTYNAME;
+ public const USERASSIGNABLE_PROPERTYNAME = SystemTagPlugin::USERASSIGNABLE_PROPERTYNAME;
+ public const CANASSIGN_PROPERTYNAME = SystemTagPlugin::CANASSIGN_PROPERTYNAME;
+ public const GROUPS_PROPERTYNAME = SystemTagPlugin::GROUPS_PROPERTYNAME;
+
+ private \Sabre\DAV\Server $server;
+ private \Sabre\DAV\Tree&MockObject $tree;
+ private ISystemTagManager&MockObject $tagManager;
+ private IGroupManager&MockObject $groupManager;
+ private IUserSession&MockObject $userSession;
+ private IRootFolder&MockObject $rootFolder;
+ private IUser&MockObject $user;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private SystemTagPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->tree = $this->createMock(Tree::class);
+
+ $this->server = new \Sabre\DAV\Server($this->tree);
+
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->user = $this->createMock(IUser::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ $this->rootFolder = $this->createMock(IRootFolder::class);
+
+ $this->plugin = new SystemTagPlugin(
+ $this->tagManager,
+ $this->groupManager,
+ $this->userSession,
+ $this->rootFolder,
+ $this->tagMapper
+ );
+ $this->plugin->initialize($this->server);
+ }
+
+ public static function getPropertiesDataProvider(): array {
+ return [
+ [
+ new SystemTag('1', 'Test', true, true),
+ [],
+ [
+ self::ID_PROPERTYNAME,
+ self::DISPLAYNAME_PROPERTYNAME,
+ self::USERVISIBLE_PROPERTYNAME,
+ self::USERASSIGNABLE_PROPERTYNAME,
+ self::CANASSIGN_PROPERTYNAME,
+ ],
+ [
+ self::ID_PROPERTYNAME => '1',
+ self::DISPLAYNAME_PROPERTYNAME => 'Test',
+ self::USERVISIBLE_PROPERTYNAME => 'true',
+ self::USERASSIGNABLE_PROPERTYNAME => 'true',
+ self::CANASSIGN_PROPERTYNAME => 'true',
+ ]
+ ],
+ [
+ new SystemTag('1', 'Test', true, false),
+ [],
+ [
+ self::ID_PROPERTYNAME,
+ self::DISPLAYNAME_PROPERTYNAME,
+ self::USERVISIBLE_PROPERTYNAME,
+ self::USERASSIGNABLE_PROPERTYNAME,
+ self::CANASSIGN_PROPERTYNAME,
+ ],
+ [
+ self::ID_PROPERTYNAME => '1',
+ self::DISPLAYNAME_PROPERTYNAME => 'Test',
+ self::USERVISIBLE_PROPERTYNAME => 'true',
+ self::USERASSIGNABLE_PROPERTYNAME => 'false',
+ self::CANASSIGN_PROPERTYNAME => 'false',
+ ]
+ ],
+ [
+ new SystemTag('1', 'Test', true, false),
+ ['group1', 'group2'],
+ [
+ self::ID_PROPERTYNAME,
+ self::GROUPS_PROPERTYNAME,
+ ],
+ [
+ self::ID_PROPERTYNAME => '1',
+ self::GROUPS_PROPERTYNAME => 'group1|group2',
+ ]
+ ],
+ [
+ new SystemTag('1', 'Test', true, true),
+ ['group1', 'group2'],
+ [
+ self::ID_PROPERTYNAME,
+ self::GROUPS_PROPERTYNAME,
+ ],
+ [
+ self::ID_PROPERTYNAME => '1',
+ // groups only returned when userAssignable is false
+ self::GROUPS_PROPERTYNAME => '',
+ ]
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('getPropertiesDataProvider')]
+ public function testGetProperties(ISystemTag $systemTag, array $groups, array $requestedProperties, array $expectedProperties): void {
+ $this->user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('admin');
+ $this->groupManager
+ ->expects($this->any())
+ ->method('isAdmin')
+ ->with('admin')
+ ->willReturn(true);
+
+ $node = $this->getMockBuilder(SystemTagNode::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getSystemTag')
+ ->willReturn($systemTag);
+
+ $this->tagManager->expects($this->any())
+ ->method('canUserAssignTag')
+ ->willReturn($systemTag->isUserAssignable());
+
+ $this->tagManager->expects($this->any())
+ ->method('getTagGroups')
+ ->willReturn($groups);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/systemtag/1')
+ ->willReturn($node);
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/systemtag/1',
+ $requestedProperties,
+ 0
+ );
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $result = $propFind->getResultForMultiStatus();
+
+ $this->assertEmpty($result[404]);
+ $this->assertEquals($expectedProperties, $result[200]);
+ }
+
+
+ public function testGetPropertiesForbidden(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $systemTag = new SystemTag('1', 'Test', true, false);
+ $requestedProperties = [
+ self::ID_PROPERTYNAME,
+ self::GROUPS_PROPERTYNAME,
+ ];
+ $this->user->expects($this->once())
+ ->method('getUID')
+ ->willReturn('admin');
+ $this->groupManager
+ ->expects($this->once())
+ ->method('isAdmin')
+ ->with('admin')
+ ->willReturn(false);
+
+ $node = $this->getMockBuilder(SystemTagNode::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getSystemTag')
+ ->willReturn($systemTag);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/systemtag/1')
+ ->willReturn($node);
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/systemtag/1',
+ $requestedProperties,
+ 0
+ );
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+ }
+
+ public function testUpdatePropertiesAdmin(): void {
+ $systemTag = new SystemTag('1', 'Test', true, false);
+ $this->user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('admin');
+ $this->groupManager
+ ->expects($this->any())
+ ->method('isAdmin')
+ ->with('admin')
+ ->willReturn(true);
+
+ $node = $this->getMockBuilder(SystemTagNode::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getSystemTag')
+ ->willReturn($systemTag);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/systemtag/1')
+ ->willReturn($node);
+
+ $node->expects($this->once())
+ ->method('update')
+ ->with('Test changed', false, true);
+
+ $this->tagManager->expects($this->once())
+ ->method('setTagGroups')
+ ->with($systemTag, ['group1', 'group2']);
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch([
+ self::DISPLAYNAME_PROPERTYNAME => 'Test changed',
+ self::USERVISIBLE_PROPERTYNAME => 'false',
+ self::USERASSIGNABLE_PROPERTYNAME => 'true',
+ self::GROUPS_PROPERTYNAME => 'group1|group2',
+ ]);
+
+ $this->plugin->handleUpdateProperties(
+ '/systemtag/1',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ // all requested properties removed, as they were processed already
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(200, $result[self::DISPLAYNAME_PROPERTYNAME]);
+ $this->assertEquals(200, $result[self::USERASSIGNABLE_PROPERTYNAME]);
+ $this->assertEquals(200, $result[self::USERVISIBLE_PROPERTYNAME]);
+ }
+
+
+ public function testUpdatePropertiesForbidden(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $systemTag = new SystemTag('1', 'Test', true, false);
+ $this->user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('admin');
+ $this->groupManager
+ ->expects($this->any())
+ ->method('isAdmin')
+ ->with('admin')
+ ->willReturn(false);
+
+ $node = $this->getMockBuilder(SystemTagNode::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getSystemTag')
+ ->willReturn($systemTag);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/systemtag/1')
+ ->willReturn($node);
+
+ $node->expects($this->never())
+ ->method('update');
+
+ $this->tagManager->expects($this->never())
+ ->method('setTagGroups');
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch([
+ self::GROUPS_PROPERTYNAME => 'group1|group2',
+ ]);
+
+ $this->plugin->handleUpdateProperties(
+ '/systemtag/1',
+ $propPatch
+ );
+
+ $propPatch->commit();
+ }
+
+ public static function createTagInsufficientPermissionsProvider(): array {
+ return [
+ [true, false, ''],
+ [false, true, ''],
+ [true, true, 'group1|group2'],
+ ];
+ }
+ #[\PHPUnit\Framework\Attributes\DataProvider('createTagInsufficientPermissionsProvider')]
+ public function testCreateNotAssignableTagAsRegularUser(bool $userVisible, bool $userAssignable, string $groups): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+ $this->expectExceptionMessage('Not sufficient permissions');
+
+ $this->user->expects($this->once())
+ ->method('getUID')
+ ->willReturn('admin');
+ $this->groupManager
+ ->expects($this->once())
+ ->method('isAdmin')
+ ->with('admin')
+ ->willReturn(false);
+
+ $requestData = [
+ 'name' => 'Test',
+ 'userVisible' => $userVisible,
+ 'userAssignable' => $userAssignable,
+ ];
+ if (!empty($groups)) {
+ $requestData['groups'] = $groups;
+ }
+ $requestData = json_encode($requestData);
+
+ $node = $this->createMock(SystemTagsByIdCollection::class);
+ $this->tagManager->expects($this->never())
+ ->method('createTag');
+ $this->tagManager->expects($this->never())
+ ->method('setTagGroups');
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/systemtags')
+ ->willReturn($node);
+
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ $request->expects($this->once())
+ ->method('getPath')
+ ->willReturn('/systemtags');
+
+ $request->expects($this->once())
+ ->method('getBodyAsString')
+ ->willReturn($requestData);
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('Content-Type')
+ ->willReturn('application/json');
+
+ $this->plugin->httpPost($request, $response);
+ }
+
+ public function testCreateTagInByIdCollectionAsRegularUser(): void {
+ $systemTag = new SystemTag('1', 'Test', true, false);
+
+ $requestData = json_encode([
+ 'name' => 'Test',
+ 'userVisible' => true,
+ 'userAssignable' => true,
+ ]);
+
+ $node = $this->createMock(SystemTagsByIdCollection::class);
+ $this->tagManager->expects($this->once())
+ ->method('createTag')
+ ->with('Test', true, true)
+ ->willReturn($systemTag);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/systemtags')
+ ->willReturn($node);
+
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ $request->expects($this->once())
+ ->method('getPath')
+ ->willReturn('/systemtags');
+
+ $request->expects($this->once())
+ ->method('getBodyAsString')
+ ->willReturn($requestData);
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('Content-Type')
+ ->willReturn('application/json');
+
+ $request->expects($this->once())
+ ->method('getUrl')
+ ->willReturn('http://example.com/dav/systemtags');
+
+ $response->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Location', 'http://example.com/dav/systemtags/1');
+
+ $this->plugin->httpPost($request, $response);
+ }
+
+ public static function createTagProvider(): array {
+ return [
+ [true, false, ''],
+ [false, false, ''],
+ [true, false, 'group1|group2'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('createTagProvider')]
+ public function testCreateTagInByIdCollection(bool $userVisible, bool $userAssignable, string $groups): void {
+ $this->user->expects($this->once())
+ ->method('getUID')
+ ->willReturn('admin');
+ $this->groupManager
+ ->expects($this->once())
+ ->method('isAdmin')
+ ->with('admin')
+ ->willReturn(true);
+
+ $systemTag = new SystemTag('1', 'Test', true, false);
+
+ $requestData = [
+ 'name' => 'Test',
+ 'userVisible' => $userVisible,
+ 'userAssignable' => $userAssignable,
+ ];
+ if (!empty($groups)) {
+ $requestData['groups'] = $groups;
+ }
+ $requestData = json_encode($requestData);
+
+ $node = $this->createMock(SystemTagsByIdCollection::class);
+ $this->tagManager->expects($this->once())
+ ->method('createTag')
+ ->with('Test', $userVisible, $userAssignable)
+ ->willReturn($systemTag);
+
+ if (!empty($groups)) {
+ $this->tagManager->expects($this->once())
+ ->method('setTagGroups')
+ ->with($systemTag, explode('|', $groups))
+ ->willReturn($systemTag);
+ } else {
+ $this->tagManager->expects($this->never())
+ ->method('setTagGroups');
+ }
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/systemtags')
+ ->willReturn($node);
+
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ $request->expects($this->once())
+ ->method('getPath')
+ ->willReturn('/systemtags');
+
+ $request->expects($this->once())
+ ->method('getBodyAsString')
+ ->willReturn($requestData);
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('Content-Type')
+ ->willReturn('application/json');
+
+ $request->expects($this->once())
+ ->method('getUrl')
+ ->willReturn('http://example.com/dav/systemtags');
+
+ $response->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Location', 'http://example.com/dav/systemtags/1');
+
+ $this->plugin->httpPost($request, $response);
+ }
+
+ public static function nodeClassProvider(): array {
+ return [
+ ['\OCA\DAV\SystemTag\SystemTagsByIdCollection'],
+ ['\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection'],
+ ];
+ }
+
+ public function testCreateTagInMappingCollection(): void {
+ $this->user->expects($this->once())
+ ->method('getUID')
+ ->willReturn('admin');
+ $this->groupManager
+ ->expects($this->once())
+ ->method('isAdmin')
+ ->with('admin')
+ ->willReturn(true);
+
+ $systemTag = new SystemTag('1', 'Test', true, false);
+
+ $requestData = json_encode([
+ 'name' => 'Test',
+ 'userVisible' => true,
+ 'userAssignable' => false,
+ ]);
+
+ $node = $this->createMock(SystemTagsObjectMappingCollection::class);
+
+ $this->tagManager->expects($this->once())
+ ->method('createTag')
+ ->with('Test', true, false)
+ ->willReturn($systemTag);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/systemtags-relations/files/12')
+ ->willReturn($node);
+
+ $node->expects($this->once())
+ ->method('createFile')
+ ->with(1);
+
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ $request->expects($this->once())
+ ->method('getPath')
+ ->willReturn('/systemtags-relations/files/12');
+
+ $request->expects($this->once())
+ ->method('getBodyAsString')
+ ->willReturn($requestData);
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('Content-Type')
+ ->willReturn('application/json');
+
+ $request->expects($this->once())
+ ->method('getBaseUrl')
+ ->willReturn('http://example.com/dav/');
+
+ $response->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Location', 'http://example.com/dav/systemtags/1');
+
+ $this->plugin->httpPost($request, $response);
+ }
+
+
+ public function testCreateTagToUnknownNode(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $node = $this->createMock(SystemTagsObjectMappingCollection::class);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willThrowException(new \Sabre\DAV\Exception\NotFound());
+
+ $this->tagManager->expects($this->never())
+ ->method('createTag');
+
+ $node->expects($this->never())
+ ->method('createFile');
+
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ $request->expects($this->once())
+ ->method('getPath')
+ ->willReturn('/systemtags-relations/files/12');
+
+ $this->plugin->httpPost($request, $response);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('nodeClassProvider')]
+ public function testCreateTagConflict(string $nodeClass): void {
+ $this->expectException(\Sabre\DAV\Exception\Conflict::class);
+
+ $this->user->expects($this->once())
+ ->method('getUID')
+ ->willReturn('admin');
+ $this->groupManager
+ ->expects($this->once())
+ ->method('isAdmin')
+ ->with('admin')
+ ->willReturn(true);
+
+ $requestData = json_encode([
+ 'name' => 'Test',
+ 'userVisible' => true,
+ 'userAssignable' => false,
+ ]);
+
+ $node = $this->createMock($nodeClass);
+ $this->tagManager->expects($this->once())
+ ->method('createTag')
+ ->with('Test', true, false)
+ ->willThrowException(new TagAlreadyExistsException('Tag already exists'));
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/systemtags')
+ ->willReturn($node);
+
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ $request->expects($this->once())
+ ->method('getPath')
+ ->willReturn('/systemtags');
+
+ $request->expects($this->once())
+ ->method('getBodyAsString')
+ ->willReturn($requestData);
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('Content-Type')
+ ->willReturn('application/json');
+
+ $this->plugin->httpPost($request, $response);
+ }
+}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php b/apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php
new file mode 100644
index 00000000000..8f7848452fe
--- /dev/null
+++ b/apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php
@@ -0,0 +1,224 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\SystemTag;
+
+use OC\SystemTag\SystemTag;
+use OCA\DAV\SystemTag\SystemTagsByIdCollection;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
+use OCP\SystemTag\TagNotFoundException;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class SystemTagsByIdCollectionTest extends \Test\TestCase {
+ private ISystemTagManager&MockObject $tagManager;
+ private IUser&MockObject $user;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ }
+
+ public function getNode(bool $isAdmin = true) {
+ $this->user = $this->createMock(IUser::class);
+ $this->user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('testuser');
+
+ /** @var IUserSession&MockObject */
+ $userSession = $this->createMock(IUserSession::class);
+ $userSession->expects($this->any())
+ ->method('getUser')
+ ->willReturn($this->user);
+
+ /** @var IGroupManager&MockObject */
+ $groupManager = $this->createMock(IGroupManager::class);
+ $groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->with('testuser')
+ ->willReturn($isAdmin);
+
+ /** @var ISystemTagObjectMapper&MockObject */
+ $tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ return new SystemTagsByIdCollection(
+ $this->tagManager,
+ $userSession,
+ $groupManager,
+ $tagMapper,
+ );
+ }
+
+ public static function adminFlagProvider(): array {
+ return [[true], [false]];
+ }
+
+
+ public function testForbiddenCreateFile(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->getNode()->createFile('555');
+ }
+
+
+ public function testForbiddenCreateDirectory(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->getNode()->createDirectory('789');
+ }
+
+ public function testGetChild(): void {
+ $tag = new SystemTag('123', 'Test', true, false);
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123'])
+ ->willReturn([$tag]);
+
+ $childNode = $this->getNode()->getChild('123');
+
+ $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $childNode);
+ $this->assertEquals('123', $childNode->getName());
+ $this->assertEquals($tag, $childNode->getSystemTag());
+ }
+
+
+ public function testGetChildInvalidName(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['invalid'])
+ ->willThrowException(new \InvalidArgumentException());
+
+ $this->getNode()->getChild('invalid');
+ }
+
+
+ public function testGetChildNotFound(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['444'])
+ ->willThrowException(new TagNotFoundException());
+
+ $this->getNode()->getChild('444');
+ }
+
+
+ public function testGetChildUserNotVisible(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $tag = new SystemTag('123', 'Test', false, false);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123'])
+ ->willReturn([$tag]);
+
+ $this->getNode(false)->getChild('123');
+ }
+
+ public function testGetChildrenAdmin(): void {
+ $tag1 = new SystemTag('123', 'One', true, false);
+ $tag2 = new SystemTag('456', 'Two', true, true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getAllTags')
+ ->with(null)
+ ->willReturn([$tag1, $tag2]);
+
+ $children = $this->getNode(true)->getChildren();
+
+ $this->assertCount(2, $children);
+
+ $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[0]);
+ $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[1]);
+ $this->assertEquals($tag1, $children[0]->getSystemTag());
+ $this->assertEquals($tag2, $children[1]->getSystemTag());
+ }
+
+ public function testGetChildrenNonAdmin(): void {
+ $tag1 = new SystemTag('123', 'One', true, false);
+ $tag2 = new SystemTag('456', 'Two', true, true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getAllTags')
+ ->with(true)
+ ->willReturn([$tag1, $tag2]);
+
+ $children = $this->getNode(false)->getChildren();
+
+ $this->assertCount(2, $children);
+
+ $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[0]);
+ $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[1]);
+ $this->assertEquals($tag1, $children[0]->getSystemTag());
+ $this->assertEquals($tag2, $children[1]->getSystemTag());
+ }
+
+ public function testGetChildrenEmpty(): void {
+ $this->tagManager->expects($this->once())
+ ->method('getAllTags')
+ ->with(null)
+ ->willReturn([]);
+ $this->assertCount(0, $this->getNode()->getChildren());
+ }
+
+ public static function childExistsProvider(): array {
+ return [
+ [true, true],
+ [false, false],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('childExistsProvider')]
+ public function testChildExists(bool $userVisible, bool $expectedResult): void {
+ $tag = new SystemTag('123', 'One', $userVisible, false);
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn($userVisible);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123'])
+ ->willReturn([$tag]);
+
+ $this->assertEquals($expectedResult, $this->getNode()->childExists('123'));
+ }
+
+ public function testChildExistsNotFound(): void {
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123'])
+ ->willThrowException(new TagNotFoundException());
+
+ $this->assertFalse($this->getNode()->childExists('123'));
+ }
+
+
+ public function testChildExistsBadRequest(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['invalid'])
+ ->willThrowException(new \InvalidArgumentException());
+
+ $this->getNode()->childExists('invalid');
+ }
+}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php b/apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php
new file mode 100644
index 00000000000..5aea1242e2a
--- /dev/null
+++ b/apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php
@@ -0,0 +1,347 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\SystemTag;
+
+use OC\SystemTag\SystemTag;
+use OCA\DAV\SystemTag\SystemTagsObjectMappingCollection;
+use OCP\IUser;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
+use OCP\SystemTag\TagNotFoundException;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
+ private ISystemTagManager&MockObject $tagManager;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private IUser&MockObject $user;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ $this->user = $this->createMock(IUser::class);
+ }
+
+ public function getNode(array $writableNodeIds = []): SystemTagsObjectMappingCollection {
+ return new SystemTagsObjectMappingCollection(
+ '111',
+ 'files',
+ $this->user,
+ $this->tagManager,
+ $this->tagMapper,
+ fn ($id): bool => in_array($id, $writableNodeIds),
+ );
+ }
+
+ public function testAssignTag(): void {
+ $tag = new SystemTag('1', 'Test', true, true);
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn(true);
+ $this->tagManager->expects($this->once())
+ ->method('canUserAssignTag')
+ ->with($tag)
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['555'])
+ ->willReturn([$tag]);
+ $this->tagMapper->expects($this->once())
+ ->method('assignTags')
+ ->with(111, 'files', '555');
+
+ $this->getNode([111])->createFile('555');
+ }
+
+ public function testAssignTagForbidden(): void {
+ $tag = new SystemTag('1', 'Test', true, true);
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn(true);
+ $this->tagManager->expects($this->once())
+ ->method('canUserAssignTag')
+ ->with($tag)
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['555'])
+ ->willReturn([$tag]);
+ $this->tagMapper->expects($this->never())
+ ->method('assignTags');
+
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->getNode()->createFile('555');
+ }
+
+ public static function permissionsProvider(): array {
+ return [
+ // invisible, tag does not exist for user
+ [false, true, '\Sabre\DAV\Exception\PreconditionFailed'],
+ // visible but static, cannot assign tag
+ [true, false, '\Sabre\DAV\Exception\Forbidden'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('permissionsProvider')]
+ public function testAssignTagNoPermission(bool $userVisible, bool $userAssignable, string $expectedException): void {
+ $tag = new SystemTag('1', 'Test', $userVisible, $userAssignable);
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn($userVisible);
+ $this->tagManager->expects($this->any())
+ ->method('canUserAssignTag')
+ ->with($tag)
+ ->willReturn($userAssignable);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['555'])
+ ->willReturn([$tag]);
+ $this->tagMapper->expects($this->never())
+ ->method('assignTags');
+
+ $thrown = null;
+ try {
+ $this->getNode()->createFile('555');
+ } catch (\Exception $e) {
+ $thrown = $e;
+ }
+
+ $this->assertInstanceOf($expectedException, $thrown);
+ }
+
+
+ public function testAssignTagNotFound(): void {
+ $this->expectException(\Sabre\DAV\Exception\PreconditionFailed::class);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['555'])
+ ->willThrowException(new TagNotFoundException());
+
+ $this->getNode()->createFile('555');
+ }
+
+
+ public function testForbiddenCreateDirectory(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->getNode()->createDirectory('789');
+ }
+
+ public function testGetChild(): void {
+ $tag = new SystemTag('555', 'TheTag', true, false);
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn(true);
+
+ $this->tagMapper->expects($this->once())
+ ->method('haveTag')
+ ->with([111], 'files', '555', true)
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['555'])
+ ->willReturn(['555' => $tag]);
+
+ $childNode = $this->getNode()->getChild('555');
+
+ $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $childNode);
+ $this->assertEquals('555', $childNode->getName());
+ }
+
+
+ public function testGetChildNonVisible(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $tag = new SystemTag('555', 'TheTag', false, false);
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn(false);
+
+ $this->tagMapper->expects($this->once())
+ ->method('haveTag')
+ ->with([111], 'files', '555', true)
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['555'])
+ ->willReturn(['555' => $tag]);
+
+ $this->getNode()->getChild('555');
+ }
+
+
+ public function testGetChildRelationNotFound(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $this->tagMapper->expects($this->once())
+ ->method('haveTag')
+ ->with([111], 'files', '777')
+ ->willReturn(false);
+
+ $this->getNode()->getChild('777');
+ }
+
+
+ public function testGetChildInvalidId(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+
+ $this->tagMapper->expects($this->once())
+ ->method('haveTag')
+ ->with([111], 'files', 'badid')
+ ->willThrowException(new \InvalidArgumentException());
+
+ $this->getNode()->getChild('badid');
+ }
+
+
+ public function testGetChildTagDoesNotExist(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $this->tagMapper->expects($this->once())
+ ->method('haveTag')
+ ->with([111], 'files', '777')
+ ->willThrowException(new TagNotFoundException());
+
+ $this->getNode()->getChild('777');
+ }
+
+ public function testGetChildren(): void {
+ $tag1 = new SystemTag('555', 'TagOne', true, false);
+ $tag2 = new SystemTag('556', 'TagTwo', true, true);
+ $tag3 = new SystemTag('557', 'InvisibleTag', false, true);
+
+ $this->tagMapper->expects($this->once())
+ ->method('getTagIdsForObjects')
+ ->with([111], 'files')
+ ->willReturn(['111' => ['555', '556', '557']]);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['555', '556', '557'])
+ ->willReturn(['555' => $tag1, '556' => $tag2, '557' => $tag3]);
+
+ $this->tagManager->expects($this->exactly(3))
+ ->method('canUserSeeTag')
+ ->willReturnCallback(function ($tag) {
+ return $tag->isUserVisible();
+ });
+
+ $children = $this->getNode()->getChildren();
+
+ $this->assertCount(2, $children);
+
+ $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[0]);
+ $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[1]);
+
+ $this->assertEquals(111, $children[0]->getObjectId());
+ $this->assertEquals('files', $children[0]->getObjectType());
+ $this->assertEquals($tag1, $children[0]->getSystemTag());
+
+ $this->assertEquals(111, $children[1]->getObjectId());
+ $this->assertEquals('files', $children[1]->getObjectType());
+ $this->assertEquals($tag2, $children[1]->getSystemTag());
+ }
+
+ public function testChildExistsWithVisibleTag(): void {
+ $tag = new SystemTag('555', 'TagOne', true, false);
+
+ $this->tagMapper->expects($this->once())
+ ->method('haveTag')
+ ->with([111], 'files', '555')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['555'])
+ ->willReturn([$tag]);
+
+ $this->assertTrue($this->getNode()->childExists('555'));
+ }
+
+ public function testChildExistsWithInvisibleTag(): void {
+ $tag = new SystemTag('555', 'TagOne', false, false);
+
+ $this->tagMapper->expects($this->once())
+ ->method('haveTag')
+ ->with([111], 'files', '555')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['555'])
+ ->willReturn([$tag]);
+
+ $this->assertFalse($this->getNode()->childExists('555'));
+ }
+
+ public function testChildExistsNotFound(): void {
+ $this->tagMapper->expects($this->once())
+ ->method('haveTag')
+ ->with([111], 'files', '555')
+ ->willReturn(false);
+
+ $this->assertFalse($this->getNode()->childExists('555'));
+ }
+
+ public function testChildExistsTagNotFound(): void {
+ $this->tagMapper->expects($this->once())
+ ->method('haveTag')
+ ->with([111], 'files', '555')
+ ->willThrowException(new TagNotFoundException());
+
+ $this->assertFalse($this->getNode()->childExists('555'));
+ }
+
+
+ public function testChildExistsInvalidId(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+
+ $this->tagMapper->expects($this->once())
+ ->method('haveTag')
+ ->with([111], 'files', '555')
+ ->willThrowException(new \InvalidArgumentException());
+
+ $this->getNode()->childExists('555');
+ }
+
+
+ public function testDelete(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->getNode()->delete();
+ }
+
+
+ public function testSetName(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->getNode()->setName('somethingelse');
+ }
+
+ public function testGetName(): void {
+ $this->assertEquals('111', $this->getNode()->getName());
+ }
+}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php b/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php
new file mode 100644
index 00000000000..301eb528436
--- /dev/null
+++ b/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php
@@ -0,0 +1,151 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\SystemTag;
+
+use OCA\DAV\SystemTag\SystemTagsObjectTypeCollection;
+use OCP\Files\Folder;
+use OCP\Files\Node;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class SystemTagsObjectTypeCollectionTest extends \Test\TestCase {
+ private ISystemTagManager&MockObject $tagManager;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private Folder&MockObject $userFolder;
+ private SystemTagsObjectTypeCollection $node;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('testuser');
+ $userSession = $this->createMock(IUserSession::class);
+ $userSession->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $groupManager = $this->createMock(IGroupManager::class);
+ $groupManager->expects($this->any())
+ ->method('isAdmin')
+ ->with('testuser')
+ ->willReturn(true);
+
+ $this->userFolder = $this->createMock(Folder::class);
+ $userFolder = $this->userFolder;
+
+ $closure = function ($name) use ($userFolder) {
+ $node = $userFolder->getFirstNodeById((int)$name);
+ return $node !== null;
+ };
+ $writeAccessClosure = function ($name) use ($userFolder) {
+ $nodes = $userFolder->getById((int)$name);
+ foreach ($nodes as $node) {
+ if (($node->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ $this->node = new SystemTagsObjectTypeCollection(
+ 'files',
+ $this->tagManager,
+ $this->tagMapper,
+ $userSession,
+ $groupManager,
+ $closure,
+ $writeAccessClosure,
+ );
+ }
+
+
+ public function testForbiddenCreateFile(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->node->createFile('555');
+ }
+
+
+ public function testForbiddenCreateDirectory(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->node->createDirectory('789');
+ }
+
+ public function testGetChild(): void {
+ $this->userFolder->expects($this->once())
+ ->method('getFirstNodeById')
+ ->with('555')
+ ->willReturn($this->createMock(Node::class));
+ $childNode = $this->node->getChild('555');
+
+ $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection', $childNode);
+ $this->assertEquals('555', $childNode->getName());
+ }
+
+
+ public function testGetChildWithoutAccess(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+
+ $this->userFolder->expects($this->once())
+ ->method('getFirstNodeById')
+ ->with('555')
+ ->willReturn(null);
+ $this->node->getChild('555');
+ }
+
+
+ public function testGetChildren(): void {
+ $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
+
+ $this->node->getChildren();
+ }
+
+ public function testChildExists(): void {
+ $this->userFolder->expects($this->once())
+ ->method('getFirstNodeById')
+ ->with('123')
+ ->willReturn($this->createMock(Node::class));
+ $this->assertTrue($this->node->childExists('123'));
+ }
+
+ public function testChildExistsWithoutAccess(): void {
+ $this->userFolder->expects($this->once())
+ ->method('getFirstNodeById')
+ ->with('555')
+ ->willReturn(null);
+ $this->assertFalse($this->node->childExists('555'));
+ }
+
+
+ public function testDelete(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->node->delete();
+ }
+
+
+ public function testSetName(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $this->node->setName('somethingelse');
+ }
+
+ public function testGetName(): void {
+ $this->assertEquals('files', $this->node->getName());
+ }
+}
diff --git a/apps/dav/tests/unit/Upload/AssemblyStreamTest.php b/apps/dav/tests/unit/Upload/AssemblyStreamTest.php
new file mode 100644
index 00000000000..ec5d0a9ab5b
--- /dev/null
+++ b/apps/dav/tests/unit/Upload/AssemblyStreamTest.php
@@ -0,0 +1,166 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Upload;
+
+use OCA\DAV\Upload\AssemblyStream;
+use Sabre\DAV\File;
+
+class AssemblyStreamTest extends \Test\TestCase {
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesNodes')]
+ public function testGetContents(string $expected, array $nodeData): void {
+ $nodes = [];
+ foreach ($nodeData as $data) {
+ $nodes[] = $this->buildNode(...$data);
+ }
+ $stream = AssemblyStream::wrap($nodes);
+ $content = stream_get_contents($stream);
+
+ $this->assertEquals($expected, $content);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesNodes')]
+ public function testGetContentsFread(string $expected, array $nodeData, int $chunkLength = 3): void {
+ $nodes = [];
+ foreach ($nodeData as $data) {
+ $nodes[] = $this->buildNode(...$data);
+ }
+ $stream = AssemblyStream::wrap($nodes);
+
+ $content = '';
+ while (!feof($stream)) {
+ $chunk = fread($stream, $chunkLength);
+ $content .= $chunk;
+ if ($chunkLength !== 3) {
+ $this->assertEquals($chunkLength, strlen($chunk));
+ }
+ }
+
+ $this->assertEquals($expected, $content);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesNodes')]
+ public function testSeek(string $expected, array $nodeData): void {
+ $nodes = [];
+ foreach ($nodeData as $data) {
+ $nodes[] = $this->buildNode(...$data);
+ }
+
+ $stream = AssemblyStream::wrap($nodes);
+
+ $offset = floor(strlen($expected) * 0.6);
+ if (fseek($stream, $offset) === -1) {
+ $this->fail('fseek failed');
+ }
+
+ $content = stream_get_contents($stream);
+ $this->assertEquals(substr($expected, $offset), $content);
+ }
+
+ public static function providesNodes(): array {
+ $data8k = self::makeData(8192);
+ $dataLess8k = self::makeData(8191);
+
+ $tonofnodes = [];
+ $tonofdata = '';
+ for ($i = 0; $i < 101; $i++) {
+ $thisdata = random_int(0, 100); // variable length and content
+ $tonofdata .= $thisdata;
+ $tonofnodes[] = [(string)$i, (string)$thisdata];
+ }
+
+ return[
+ 'one node zero bytes' => [
+ '', [
+ ['0', ''],
+ ]],
+ 'one node only' => [
+ '1234567890', [
+ ['0', '1234567890'],
+ ]],
+ 'one node buffer boundary' => [
+ $data8k, [
+ ['0', $data8k],
+ ]],
+ 'two nodes' => [
+ '1234567890', [
+ ['1', '67890'],
+ ['0', '12345'],
+ ]],
+ 'two nodes end on buffer boundary' => [
+ $data8k . $data8k, [
+ ['1', $data8k],
+ ['0', $data8k],
+ ]],
+ 'two nodes with one on buffer boundary' => [
+ $data8k . $dataLess8k, [
+ ['1', $dataLess8k],
+ ['0', $data8k],
+ ]],
+ 'two nodes on buffer boundary plus one byte' => [
+ $data8k . 'X' . $data8k, [
+ ['1', $data8k],
+ ['0', $data8k . 'X'],
+ ]],
+ 'two nodes on buffer boundary plus one byte at the end' => [
+ $data8k . $data8k . 'X', [
+ ['1', $data8k . 'X'],
+ ['0', $data8k],
+ ]],
+ 'a ton of nodes' => [
+ $tonofdata, $tonofnodes
+ ],
+ 'one read over multiple nodes' => [
+ '1234567890', [
+ ['0', '1234'],
+ ['1', '5678'],
+ ['2', '90'],
+ ], 10],
+ 'two reads over multiple nodes' => [
+ '1234567890', [
+ ['0', '1234'],
+ ['1', '5678'],
+ ['2', '90'],
+ ], 5],
+ ];
+ }
+
+ private static function makeData(int $count): string {
+ $data = '';
+ $base = '1234567890';
+ $j = 0;
+ for ($i = 0; $i < $count; $i++) {
+ $data .= $base[$j];
+ $j++;
+ if (!isset($base[$j])) {
+ $j = 0;
+ }
+ }
+ return $data;
+ }
+
+ private function buildNode(string $name, string $data) {
+ $node = $this->getMockBuilder(File::class)
+ ->onlyMethods(['getName', 'get', 'getSize'])
+ ->getMock();
+
+ $node->expects($this->any())
+ ->method('getName')
+ ->willReturn($name);
+
+ $node->expects($this->any())
+ ->method('get')
+ ->willReturn($data);
+
+ $node->expects($this->any())
+ ->method('getSize')
+ ->willReturn(strlen($data));
+
+ return $node;
+ }
+}
diff --git a/apps/dav/tests/unit/Upload/ChunkingPluginTest.php b/apps/dav/tests/unit/Upload/ChunkingPluginTest.php
new file mode 100644
index 00000000000..00ed7657dd3
--- /dev/null
+++ b/apps/dav/tests/unit/Upload/ChunkingPluginTest.php
@@ -0,0 +1,189 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Upload;
+
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Upload\ChunkingPlugin;
+use OCA\DAV\Upload\FutureFile;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+class ChunkingPluginTest extends TestCase {
+ private \Sabre\DAV\Server&MockObject $server;
+ private \Sabre\DAV\Tree&MockObject $tree;
+ private ChunkingPlugin $plugin;
+ private RequestInterface&MockObject $request;
+ private ResponseInterface&MockObject $response;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->server = $this->createMock('\Sabre\DAV\Server');
+ $this->tree = $this->createMock('\Sabre\DAV\Tree');
+
+ $this->server->tree = $this->tree;
+ $this->plugin = new ChunkingPlugin();
+
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->response = $this->createMock(ResponseInterface::class);
+ $this->server->httpRequest = $this->request;
+ $this->server->httpResponse = $this->response;
+
+ $this->plugin->initialize($this->server);
+ }
+
+ public function testBeforeMoveFutureFileSkip(): void {
+ $node = $this->createMock(Directory::class);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('source')
+ ->willReturn($node);
+ $this->response->expects($this->never())
+ ->method('setStatus');
+
+ $this->assertNull($this->plugin->beforeMove('source', 'target'));
+ }
+
+ public function testBeforeMoveDestinationIsDirectory(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+ $this->expectExceptionMessage('The given destination target is a directory.');
+
+ $sourceNode = $this->createMock(FutureFile::class);
+ $targetNode = $this->createMock(Directory::class);
+
+ $this->tree->expects($this->exactly(2))
+ ->method('getNodeForPath')
+ ->willReturnMap([
+ ['source', $sourceNode],
+ ['target', $targetNode],
+ ]);
+ $this->response->expects($this->never())
+ ->method('setStatus');
+
+ $this->assertNull($this->plugin->beforeMove('source', 'target'));
+ }
+
+ public function testBeforeMoveFutureFileSkipNonExisting(): void {
+ $sourceNode = $this->createMock(FutureFile::class);
+ $sourceNode->expects($this->once())
+ ->method('getSize')
+ ->willReturn(4);
+
+ $calls = [
+ ['source', $sourceNode],
+ ['target', new NotFound()],
+ ];
+ $this->tree->expects($this->exactly(2))
+ ->method('getNodeForPath')
+ ->willReturnCallback(function (string $path) use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertSame($expected[0], $path);
+ if ($expected[1] instanceof \Throwable) {
+ throw $expected[1];
+ }
+ return $expected[1];
+ });
+ $this->tree->expects($this->any())
+ ->method('nodeExists')
+ ->with('target')
+ ->willReturn(false);
+ $this->response->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Length', '0');
+ $this->response->expects($this->once())
+ ->method('setStatus')
+ ->with(201);
+ $this->request->expects($this->once())
+ ->method('getHeader')
+ ->with('OC-Total-Length')
+ ->willReturn(4);
+
+ $this->assertFalse($this->plugin->beforeMove('source', 'target'));
+ }
+
+ public function testBeforeMoveFutureFileMoveIt(): void {
+ $sourceNode = $this->createMock(FutureFile::class);
+ $sourceNode->expects($this->once())
+ ->method('getSize')
+ ->willReturn(4);
+
+ $calls = [
+ ['source', $sourceNode],
+ ['target', new NotFound()],
+ ];
+ $this->tree->expects($this->exactly(2))
+ ->method('getNodeForPath')
+ ->willReturnCallback(function (string $path) use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertSame($expected[0], $path);
+ if ($expected[1] instanceof \Throwable) {
+ throw $expected[1];
+ }
+ return $expected[1];
+ });
+
+ $this->tree->expects($this->any())
+ ->method('nodeExists')
+ ->with('target')
+ ->willReturn(true);
+ $this->tree->expects($this->once())
+ ->method('move')
+ ->with('source', 'target');
+
+ $this->response->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Length', '0');
+ $this->response->expects($this->once())
+ ->method('setStatus')
+ ->with(204);
+ $this->request->expects($this->once())
+ ->method('getHeader')
+ ->with('OC-Total-Length')
+ ->willReturn('4');
+
+ $this->assertFalse($this->plugin->beforeMove('source', 'target'));
+ }
+
+
+ public function testBeforeMoveSizeIsWrong(): void {
+ $this->expectException(\Sabre\DAV\Exception\BadRequest::class);
+ $this->expectExceptionMessage('Chunks on server do not sum up to 4 but to 3 bytes');
+
+ $sourceNode = $this->createMock(FutureFile::class);
+ $sourceNode->expects($this->once())
+ ->method('getSize')
+ ->willReturn(3);
+
+ $calls = [
+ ['source', $sourceNode],
+ ['target', new NotFound()],
+ ];
+ $this->tree->expects($this->exactly(2))
+ ->method('getNodeForPath')
+ ->willReturnCallback(function (string $path) use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertSame($expected[0], $path);
+ if ($expected[1] instanceof \Throwable) {
+ throw $expected[1];
+ }
+ return $expected[1];
+ });
+
+ $this->request->expects($this->once())
+ ->method('getHeader')
+ ->with('OC-Total-Length')
+ ->willReturn('4');
+
+ $this->assertFalse($this->plugin->beforeMove('source', 'target'));
+ }
+}
diff --git a/apps/dav/tests/unit/Upload/FutureFileTest.php b/apps/dav/tests/unit/Upload/FutureFileTest.php
new file mode 100644
index 00000000000..1409df937c0
--- /dev/null
+++ b/apps/dav/tests/unit/Upload/FutureFileTest.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Upload;
+
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Upload\FutureFile;
+
+class FutureFileTest extends \Test\TestCase {
+ public function testGetContentType(): void {
+ $f = $this->mockFutureFile();
+ $this->assertEquals('application/octet-stream', $f->getContentType());
+ }
+
+ public function testGetETag(): void {
+ $f = $this->mockFutureFile();
+ $this->assertEquals('1234567890', $f->getETag());
+ }
+
+ public function testGetName(): void {
+ $f = $this->mockFutureFile();
+ $this->assertEquals('foo.txt', $f->getName());
+ }
+
+ public function testGetLastModified(): void {
+ $f = $this->mockFutureFile();
+ $this->assertEquals(12121212, $f->getLastModified());
+ }
+
+ public function testGetSize(): void {
+ $f = $this->mockFutureFile();
+ $this->assertEquals(0, $f->getSize());
+ }
+
+ public function testGet(): void {
+ $f = $this->mockFutureFile();
+ $stream = $f->get();
+ $this->assertTrue(is_resource($stream));
+ }
+
+ public function testDelete(): void {
+ $d = $this->getMockBuilder(Directory::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['delete'])
+ ->getMock();
+
+ $d->expects($this->once())
+ ->method('delete');
+
+ $f = new FutureFile($d, 'foo.txt');
+ $f->delete();
+ }
+
+
+ public function testPut(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $f = $this->mockFutureFile();
+ $f->put('');
+ }
+
+
+ public function testSetName(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $f = $this->mockFutureFile();
+ $f->setName('');
+ }
+
+ private function mockFutureFile(): FutureFile {
+ $d = $this->getMockBuilder(Directory::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getETag', 'getLastModified', 'getChildren'])
+ ->getMock();
+
+ $d->expects($this->any())
+ ->method('getETag')
+ ->willReturn('1234567890');
+
+ $d->expects($this->any())
+ ->method('getLastModified')
+ ->willReturn(12121212);
+
+ $d->expects($this->any())
+ ->method('getChildren')
+ ->willReturn([]);
+
+ return new FutureFile($d, 'foo.txt');
+ }
+}
diff --git a/apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php b/apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php
new file mode 100644
index 00000000000..baae839c8da
--- /dev/null
+++ b/apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php
@@ -0,0 +1,133 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Upload;
+
+use Generator;
+use OCA\DAV\Upload\UploadAutoMkcolPlugin;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\ICollection;
+use Sabre\DAV\INode;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+class UploadAutoMkcolPluginTest extends TestCase {
+
+ private Tree&MockObject $tree;
+ private RequestInterface&MockObject $request;
+ private ResponseInterface&MockObject $response;
+
+ public static function dataMissingHeaderShouldReturnTrue(): Generator {
+ yield 'missing X-NC-WebDAV-Auto-Mkcol header' => [null];
+ yield 'empty X-NC-WebDAV-Auto-Mkcol header' => [''];
+ yield 'invalid X-NC-WebDAV-Auto-Mkcol header' => ['enable'];
+ }
+
+ public function testBeforeMethodWithRootNodeNotAnICollectionShouldReturnTrue(): void {
+ $this->request->method('getHeader')->willReturn('1');
+ $this->request->expects(self::once())
+ ->method('getPath')
+ ->willReturn('/non-relevant/path.txt');
+ $this->tree->expects(self::once())
+ ->method('nodeExists')
+ ->with('/non-relevant')
+ ->willReturn(false);
+
+ $mockNode = $this->getMockBuilder(INode::class);
+ $this->tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->willReturn($mockNode);
+
+ $return = $this->plugin->beforeMethod($this->request, $this->response);
+ $this->assertTrue($return);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataMissingHeaderShouldReturnTrue')]
+ public function testBeforeMethodWithMissingHeaderShouldReturnTrue(?string $header): void {
+ $this->request->expects(self::once())
+ ->method('getHeader')
+ ->with('X-NC-WebDAV-Auto-Mkcol')
+ ->willReturn($header);
+
+ $this->request->expects(self::never())
+ ->method('getPath');
+
+ $return = $this->plugin->beforeMethod($this->request, $this->response);
+ self::assertTrue($return);
+ }
+
+ public function testBeforeMethodWithExistingPathShouldReturnTrue(): void {
+ $this->request->method('getHeader')->willReturn('1');
+ $this->request->expects(self::once())
+ ->method('getPath')
+ ->willReturn('/files/user/deep/image.jpg');
+ $this->tree->expects(self::once())
+ ->method('nodeExists')
+ ->with('/files/user/deep')
+ ->willReturn(true);
+
+ $this->tree->expects(self::never())
+ ->method('getNodeForPath');
+
+ $return = $this->plugin->beforeMethod($this->request, $this->response);
+ self::assertTrue($return);
+ }
+
+ public function testBeforeMethodShouldSucceed(): void {
+ $this->request->method('getHeader')->willReturn('1');
+ $this->request->expects(self::once())
+ ->method('getPath')
+ ->willReturn('/files/user/my/deep/path/image.jpg');
+ $this->tree->expects(self::once())
+ ->method('nodeExists')
+ ->with('/files/user/my/deep/path')
+ ->willReturn(false);
+
+ $mockNode = $this->createMock(ICollection::class);
+ $this->tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/files')
+ ->willReturn($mockNode);
+ $mockNode->expects(self::exactly(4))
+ ->method('childExists')
+ ->willReturnMap([
+ ['user', true],
+ ['my', true],
+ ['deep', false],
+ ['path', false],
+ ]);
+ $mockNode->expects(self::exactly(2))
+ ->method('createDirectory');
+ $mockNode->expects(self::exactly(4))
+ ->method('getChild')
+ ->willReturn($mockNode);
+
+ $return = $this->plugin->beforeMethod($this->request, $this->response);
+ self::assertTrue($return);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $server = $this->createMock(Server::class);
+ $this->tree = $this->createMock(Tree::class);
+
+ $server->tree = $this->tree;
+ $this->plugin = new UploadAutoMkcolPlugin();
+
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->response = $this->createMock(ResponseInterface::class);
+ $server->httpRequest = $this->request;
+ $server->httpResponse = $this->response;
+
+ $this->plugin->initialize($server);
+ }
+}
diff --git a/apps/dav/tests/unit/appinfo/applicationtest.php b/apps/dav/tests/unit/appinfo/applicationtest.php
deleted file mode 100644
index 7f533a185df..00000000000
--- a/apps/dav/tests/unit/appinfo/applicationtest.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\AppInfo;
-
-use OCA\Dav\AppInfo\Application;
-use OCP\Contacts\IManager;
-use Test\TestCase;
-
-/**
- * Class ApplicationTest
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\Unit\AppInfo
- */
-class ApplicationTest extends TestCase {
- public function test() {
- $app = new Application();
- $c = $app->getContainer();
-
- // assert service instances in the container are properly setup
- $s = $c->query('ContactsManager');
- $this->assertInstanceOf('OCA\DAV\CardDAV\ContactsManager', $s);
- $s = $c->query('CardDavBackend');
- $this->assertInstanceOf('OCA\DAV\CardDAV\CardDavBackend', $s);
- }
-
- public function testContactsManagerSetup() {
- $app = new Application();
- $c = $app->getContainer();
- $c->registerService('CardDavBackend', function($c) {
- $service = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock();
- $service->method('getAddressBooksForUser')->willReturn([]);
- return $service;
- });
-
- // assert setupContactsProvider() is proper
- /** @var IManager | \PHPUnit_Framework_MockObject_MockObject $cm */
- $cm = $this->getMockBuilder('OCP\Contacts\IManager')->disableOriginalConstructor()->getMock();
- $app->setupContactsProvider($cm, 'xxx');
- $this->assertTrue(true);
- }
-}
diff --git a/apps/dav/tests/unit/bootstrap.php b/apps/dav/tests/unit/bootstrap.php
index f6733bc7a3e..ee76bb6677b 100644
--- a/apps/dav/tests/unit/bootstrap.php
+++ b/apps/dav/tests/unit/bootstrap.php
@@ -1,33 +1,21 @@
<?php
+
+declare(strict_types=1);
+
/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-if (!defined('PHPUNIT_RUN')) {
- define('PHPUNIT_RUN', 1);
-}
-require_once __DIR__.'/../../../../lib/base.php';
+use OCP\App\IAppManager;
+use OCP\Server;
-if(!class_exists('PHPUnit_Framework_TestCase')) {
- require_once('PHPUnit/Autoload.php');
+if (!defined('PHPUNIT_RUN')) {
+ define('PHPUNIT_RUN', 1);
}
-\OC_App::loadApp('dav');
+require_once __DIR__ . '/../../../../lib/base.php';
+require_once __DIR__ . '/../../../../tests/autoload.php';
-OC_Hook::clear();
+Server::get(IAppManager::class)->loadApp('dav');
diff --git a/apps/dav/tests/unit/caldav/caldavbackendtest.php b/apps/dav/tests/unit/caldav/caldavbackendtest.php
deleted file mode 100644
index 87a700a473d..00000000000
--- a/apps/dav/tests/unit/caldav/caldavbackendtest.php
+++ /dev/null
@@ -1,491 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace Tests\Connector\Sabre;
-
-use DateTime;
-use DateTimeZone;
-use OCA\DAV\CalDAV\CalDavBackend;
-use OCA\DAV\CalDAV\Calendar;
-use OCA\DAV\Connector\Sabre\Principal;
-use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
-use Sabre\DAV\PropPatch;
-use Sabre\DAV\Xml\Property\Href;
-use Sabre\DAVACL\IACL;
-use Test\TestCase;
-
-/**
- * Class CalDavBackendTest
- *
- * @group DB
- *
- * @package Tests\Connector\Sabre
- */
-class CalDavBackendTest extends TestCase {
-
- /** @var CalDavBackend */
- private $backend;
-
- /** @var Principal | \PHPUnit_Framework_MockObject_MockObject */
- private $principal;
-
- const UNIT_TEST_USER = 'principals/users/caldav-unit-test';
- const UNIT_TEST_USER1 = 'principals/users/caldav-unit-test1';
- const UNIT_TEST_GROUP = 'principals/groups/caldav-unit-test-group';
-
- public function setUp() {
- parent::setUp();
-
- $this->principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal')
- ->disableOriginalConstructor()
- ->setMethods(['getPrincipalByPath', 'getGroupMembership'])
- ->getMock();
- $this->principal->expects($this->any())->method('getPrincipalByPath')
- ->willReturn([
- 'uri' => 'principals/best-friend'
- ]);
- $this->principal->expects($this->any())->method('getGroupMembership')
- ->withAnyParameters()
- ->willReturn([self::UNIT_TEST_GROUP]);
-
- $db = \OC::$server->getDatabaseConnection();
- $this->backend = new CalDavBackend($db, $this->principal);
-
- $this->tearDown();
- }
-
- public function tearDown() {
- parent::tearDown();
-
- if (is_null($this->backend)) {
- return;
- }
- $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
- foreach ($books as $book) {
- $this->backend->deleteCalendar($book['id']);
- }
- $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
- foreach ($subscriptions as $subscription) {
- $this->backend->deleteSubscription($subscription['id']);
- }
- }
-
- public function testCalendarOperations() {
-
- $calendarId = $this->createTestCalendar();
-
- // update it's display name
- $patch = new PropPatch([
- '{DAV:}displayname' => 'Unit test',
- '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar used for unit testing'
- ]);
- $this->backend->updateCalendar($calendarId, $patch);
- $patch->commit();
- $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($books));
- $this->assertEquals('Unit test', $books[0]['{DAV:}displayname']);
- $this->assertEquals('Calendar used for unit testing', $books[0]['{urn:ietf:params:xml:ns:caldav}calendar-description']);
-
- // delete the address book
- $this->backend->deleteCalendar($books[0]['id']);
- $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
- $this->assertEquals(0, count($books));
- }
-
- public function providesSharingData() {
- return [
- [true, true, true, false, [
- [
- 'href' => 'principal:' . self::UNIT_TEST_USER1,
- 'readOnly' => false
- ],
- [
- 'href' => 'principal:' . self::UNIT_TEST_GROUP,
- 'readOnly' => true
- ]
- ]],
- [true, false, false, false, [
- [
- 'href' => 'principal:' . self::UNIT_TEST_USER1,
- 'readOnly' => true
- ],
- ]],
-
- ];
- }
-
- /**
- * @dataProvider providesSharingData
- */
- public function testCalendarSharing($userCanRead, $userCanWrite, $groupCanRead, $groupCanWrite, $add) {
-
- $calendarId = $this->createTestCalendar();
- $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($books));
- $calendar = new Calendar($this->backend, $books[0]);
- $this->backend->updateShares($calendar, $add, []);
- $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER1);
- $this->assertEquals(1, count($books));
- $calendar = new Calendar($this->backend, $books[0]);
- $acl = $calendar->getACL();
- $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}read', $acl);
- $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}write', $acl);
- $this->assertAccess($userCanRead, self::UNIT_TEST_USER1, '{DAV:}read', $acl);
- $this->assertAccess($userCanWrite, self::UNIT_TEST_USER1, '{DAV:}write', $acl);
- $this->assertAccess($groupCanRead, self::UNIT_TEST_GROUP, '{DAV:}read', $acl);
- $this->assertAccess($groupCanWrite, self::UNIT_TEST_GROUP, '{DAV:}write', $acl);
- $this->assertEquals(self::UNIT_TEST_USER, $calendar->getOwner());
-
- // test acls on the child
- $uri = $this->getUniqueID('calobj');
- $calData = <<<'EOD'
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:ownCloud Calendar
-BEGIN:VEVENT
-CREATED;VALUE=DATE-TIME:20130910T125139Z
-UID:47d15e3ec8
-LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
-DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
-SUMMARY:Test Event
-DTSTART;VALUE=DATE-TIME:20130912T130000Z
-DTEND;VALUE=DATE-TIME:20130912T140000Z
-CLASS:PUBLIC
-END:VEVENT
-END:VCALENDAR
-EOD;
-
- $this->backend->createCalendarObject($calendarId, $uri, $calData);
-
- /** @var IACL $child */
- $child = $calendar->getChild($uri);
- $acl = $child->getACL();
- $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}read', $acl);
- $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}write', $acl);
- $this->assertAccess($userCanRead, self::UNIT_TEST_USER1, '{DAV:}read', $acl);
- $this->assertAccess($userCanWrite, self::UNIT_TEST_USER1, '{DAV:}write', $acl);
- $this->assertAccess($groupCanRead, self::UNIT_TEST_GROUP, '{DAV:}read', $acl);
- $this->assertAccess($groupCanWrite, self::UNIT_TEST_GROUP, '{DAV:}write', $acl);
-
- // delete the address book
- $this->backend->deleteCalendar($books[0]['id']);
- $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
- $this->assertEquals(0, count($books));
- }
-
- public function testCalendarObjectsOperations() {
-
- $calendarId = $this->createTestCalendar();
-
- // create a card
- $uri = $this->getUniqueID('calobj');
- $calData = <<<'EOD'
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:ownCloud Calendar
-BEGIN:VEVENT
-CREATED;VALUE=DATE-TIME:20130910T125139Z
-UID:47d15e3ec8
-LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
-DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
-SUMMARY:Test Event
-DTSTART;VALUE=DATE-TIME:20130912T130000Z
-DTEND;VALUE=DATE-TIME:20130912T140000Z
-CLASS:PUBLIC
-END:VEVENT
-END:VCALENDAR
-EOD;
-
- $this->backend->createCalendarObject($calendarId, $uri, $calData);
-
- // get all the cards
- $calendarObjects = $this->backend->getCalendarObjects($calendarId);
- $this->assertEquals(1, count($calendarObjects));
- $this->assertEquals($calendarId, $calendarObjects[0]['calendarid']);
-
- // get the cards
- $calendarObject = $this->backend->getCalendarObject($calendarId, $uri);
- $this->assertNotNull($calendarObject);
- $this->assertArrayHasKey('id', $calendarObject);
- $this->assertArrayHasKey('uri', $calendarObject);
- $this->assertArrayHasKey('lastmodified', $calendarObject);
- $this->assertArrayHasKey('etag', $calendarObject);
- $this->assertArrayHasKey('size', $calendarObject);
- $this->assertEquals($calData, $calendarObject['calendardata']);
-
- // update the card
- $calData = <<<'EOD'
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:ownCloud Calendar
-BEGIN:VEVENT
-CREATED;VALUE=DATE-TIME:20130910T125139Z
-UID:47d15e3ec8
-LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
-DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
-SUMMARY:Test Event
-DTSTART;VALUE=DATE-TIME:20130912T130000Z
-DTEND;VALUE=DATE-TIME:20130912T140000Z
-END:VEVENT
-END:VCALENDAR
-EOD;
- $this->backend->updateCalendarObject($calendarId, $uri, $calData);
- $calendarObject = $this->backend->getCalendarObject($calendarId, $uri);
- $this->assertEquals($calData, $calendarObject['calendardata']);
-
- // delete the card
- $this->backend->deleteCalendarObject($calendarId, $uri);
- $calendarObjects = $this->backend->getCalendarObjects($calendarId);
- $this->assertEquals(0, count($calendarObjects));
- }
-
- public function testMultiCalendarObjects() {
-
- $calendarId = $this->createTestCalendar();
-
- // create an event
- $calData = <<<'EOD'
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:ownCloud Calendar
-BEGIN:VEVENT
-CREATED;VALUE=DATE-TIME:20130910T125139Z
-UID:47d15e3ec8
-LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
-DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
-SUMMARY:Test Event
-DTSTART;VALUE=DATE-TIME:20130912T130000Z
-DTEND;VALUE=DATE-TIME:20130912T140000Z
-CLASS:PUBLIC
-END:VEVENT
-END:VCALENDAR
-EOD;
- $uri0 = $this->getUniqueID('card');
- $this->backend->createCalendarObject($calendarId, $uri0, $calData);
- $uri1 = $this->getUniqueID('card');
- $this->backend->createCalendarObject($calendarId, $uri1, $calData);
- $uri2 = $this->getUniqueID('card');
- $this->backend->createCalendarObject($calendarId, $uri2, $calData);
-
- // get all the cards
- $calendarObjects = $this->backend->getCalendarObjects($calendarId);
- $this->assertEquals(3, count($calendarObjects));
-
- // get the cards
- $calendarObjects = $this->backend->getMultipleCalendarObjects($calendarId, [$uri1, $uri2]);
- $this->assertEquals(2, count($calendarObjects));
- foreach($calendarObjects as $card) {
- $this->assertArrayHasKey('id', $card);
- $this->assertArrayHasKey('uri', $card);
- $this->assertArrayHasKey('lastmodified', $card);
- $this->assertArrayHasKey('etag', $card);
- $this->assertArrayHasKey('size', $card);
- $this->assertEquals($calData, $card['calendardata']);
- }
-
- // delete the card
- $this->backend->deleteCalendarObject($calendarId, $uri0);
- $this->backend->deleteCalendarObject($calendarId, $uri1);
- $this->backend->deleteCalendarObject($calendarId, $uri2);
- $calendarObjects = $this->backend->getCalendarObjects($calendarId);
- $this->assertEquals(0, count($calendarObjects));
- }
-
- /**
- * @dataProvider providesCalendarQueryParameters
- */
- public function testCalendarQuery($expectedEventsInResult, $propFilters, $compFilter) {
- $calendarId = $this->createTestCalendar();
- $events = [];
- $events[0] = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z');
- $events[1] = $this->createEvent($calendarId, '20130912T150000Z', '20130912T170000Z');
- $events[2] = $this->createEvent($calendarId, '20130912T173000Z', '20130912T220000Z');
-
- $result = $this->backend->calendarQuery($calendarId, [
- 'name' => '',
- 'prop-filters' => $propFilters,
- 'comp-filters' => $compFilter
- ]);
-
- $expectedEventsInResult = array_map(function($index) use($events) {
- return $events[$index];
- }, $expectedEventsInResult);
- $this->assertEquals($expectedEventsInResult, $result, '', 0.0, 10, true);
- }
-
- public function testGetCalendarObjectByUID() {
- $calendarId = $this->createTestCalendar();
- $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z');
-
- $co = $this->backend->getCalendarObjectByUID(self::UNIT_TEST_USER, '47d15e3ec8');
- $this->assertNotNull($co);
- }
-
- public function providesCalendarQueryParameters() {
- return [
- 'all' => [[0, 1, 2], [], []],
- 'only-todos' => [[], ['name' => 'VTODO'], []],
- 'only-events' => [[0, 1, 2], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => null], 'prop-filters' => []]],],
- 'start' => [[1, 2], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],],
- 'end' => [[0], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC'))], 'prop-filters' => []]],],
- ];
- }
-
- private function createTestCalendar() {
- $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', [
- '{http://apple.com/ns/ical/}calendar-color' => '#1C4587FF'
- ]);
- $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($calendars));
- $this->assertEquals(self::UNIT_TEST_USER, $calendars[0]['principaluri']);
- /** @var SupportedCalendarComponentSet $components */
- $components = $calendars[0]['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'];
- $this->assertEquals(['VEVENT','VTODO'], $components->getValue());
- $color = $calendars[0]['{http://apple.com/ns/ical/}calendar-color'];
- $this->assertEquals('#1C4587FF', $color);
- $this->assertEquals('Example', $calendars[0]['uri']);
- $this->assertEquals('Example', $calendars[0]['{DAV:}displayname']);
- $calendarId = $calendars[0]['id'];
-
- return $calendarId;
- }
-
- private function createEvent($calendarId, $start = '20130912T130000Z', $end = '20130912T140000Z') {
-
- $calData = <<<EOD
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:ownCloud Calendar
-BEGIN:VEVENT
-CREATED;VALUE=DATE-TIME:20130910T125139Z
-UID:47d15e3ec8
-LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
-DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
-SUMMARY:Test Event
-DTSTART;VALUE=DATE-TIME:$start
-DTEND;VALUE=DATE-TIME:$end
-CLASS:PUBLIC
-END:VEVENT
-END:VCALENDAR
-EOD;
- $uri0 = $this->getUniqueID('event');
- $this->backend->createCalendarObject($calendarId, $uri0, $calData);
-
- return $uri0;
- }
-
- public function testSyncSupport() {
- $calendarId = $this->createTestCalendar();
-
- // fist call without synctoken
- $changes = $this->backend->getChangesForCalendar($calendarId, '', 1);
- $syncToken = $changes['syncToken'];
-
- // add a change
- $event = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z');
-
- // look for changes
- $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 1);
- $this->assertEquals($event, $changes['added'][0]);
- }
-
- public function testSubscriptions() {
- $id = $this->backend->createSubscription(self::UNIT_TEST_USER, 'Subscription', [
- '{http://calendarserver.org/ns/}source' => new Href('test-source')
- ]);
-
- $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($subscriptions));
- $this->assertEquals($id, $subscriptions[0]['id']);
-
- $patch = new PropPatch([
- '{DAV:}displayname' => 'Unit test',
- ]);
- $this->backend->updateSubscription($id, $patch);
- $patch->commit();
-
- $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($subscriptions));
- $this->assertEquals($id, $subscriptions[0]['id']);
- $this->assertEquals('Unit test', $subscriptions[0]['{DAV:}displayname']);
-
- $this->backend->deleteSubscription($id);
- $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
- $this->assertEquals(0, count($subscriptions));
- }
-
- public function testScheduling() {
- $this->backend->createSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule', '');
-
- $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($sos));
-
- $so = $this->backend->getSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule');
- $this->assertNotNull($so);
-
- $this->backend->deleteSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule');
-
- $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER);
- $this->assertEquals(0, count($sos));
- }
-
- /**
- * @dataProvider providesCalDataForGetDenormalizedData
- */
- public function testGetDenormalizedData($expectedFirstOccurance, $calData) {
- $actual = $this->invokePrivate($this->backend, 'getDenormalizedData', [$calData]);
- $this->assertEquals($expectedFirstOccurance, $actual['firstOccurence']);
- }
-
- public function providesCalDataForGetDenormalizedData() {
- return [
- [0, "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:413F269B-B51B-46B1-AFB6-40055C53A4DC\r\nDTSTAMP:20160309T095056Z\r\nDTSTART;VALUE=DATE:16040222\r\nDTEND;VALUE=DATE:16040223\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:SUMMARY\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
- [null, "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:413F269B-B51B-46B1-AFB6-40055C53A4DC\r\nDTSTAMP:20160309T095056Z\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:SUMMARY\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"]
- ];
- }
-
- private function assertAcl($principal, $privilege, $acl) {
- foreach($acl as $a) {
- if ($a['principal'] === $principal && $a['privilege'] === $privilege) {
- $this->assertTrue(true);
- return;
- }
- }
- $this->fail("ACL does not contain $principal / $privilege");
- }
-
- private function assertNotAcl($principal, $privilege, $acl) {
- foreach($acl as $a) {
- if ($a['principal'] === $principal && $a['privilege'] === $privilege) {
- $this->fail("ACL contains $principal / $privilege");
- return;
- }
- }
- $this->assertTrue(true);
- }
-
- private function assertAccess($shouldHaveAcl, $principal, $privilege, $acl) {
- if ($shouldHaveAcl) {
- $this->assertAcl($principal, $privilege, $acl);
- } else {
- $this->assertNotAcl($principal, $privilege, $acl);
- }
- }
-}
diff --git a/apps/dav/tests/unit/caldav/calendartest.php b/apps/dav/tests/unit/caldav/calendartest.php
deleted file mode 100644
index 9e0c3c6c7e4..00000000000
--- a/apps/dav/tests/unit/caldav/calendartest.php
+++ /dev/null
@@ -1,166 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\CalDAV;
-
-use OCA\DAV\CalDAV\CalDavBackend;
-use OCA\DAV\CalDAV\Calendar;
-use Sabre\DAV\PropPatch;
-use Test\TestCase;
-
-class CalendarTest extends TestCase {
-
- public function testDelete() {
- /** @var \PHPUnit_Framework_MockObject_MockObject | CalDavBackend $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock();
- $backend->expects($this->once())->method('updateShares');
- $backend->expects($this->any())->method('getShares')->willReturn([
- ['href' => 'principal:user2']
- ]);
- $calendarInfo = [
- '{http://owncloud.org/ns}owner-principal' => 'user1',
- 'principaluri' => 'user2',
- 'id' => 666,
- 'uri' => 'cal',
- ];
- $c = new Calendar($backend, $calendarInfo);
- $c->delete();
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testDeleteFromGroup() {
- /** @var \PHPUnit_Framework_MockObject_MockObject | CalDavBackend $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock();
- $backend->expects($this->never())->method('updateShares');
- $backend->expects($this->any())->method('getShares')->willReturn([
- ['href' => 'principal:group2']
- ]);
- $calendarInfo = [
- '{http://owncloud.org/ns}owner-principal' => 'user1',
- 'principaluri' => 'user2',
- 'id' => 666,
- 'uri' => 'cal',
- ];
- $c = new Calendar($backend, $calendarInfo);
- $c->delete();
- }
-
- public function dataPropPatch() {
- return [
- [[], true],
- [[
- '{http://owncloud.org/ns}calendar-enabled' => true,
- ], false],
- [[
- '{DAV:}displayname' => true,
- ], true],
- [[
- '{DAV:}displayname' => true,
- '{http://owncloud.org/ns}calendar-enabled' => true,
- ], true],
- ];
- }
-
- /**
- * @dataProvider dataPropPatch
- */
- public function testPropPatch($mutations, $throws) {
- /** @var \PHPUnit_Framework_MockObject_MockObject | CalDavBackend $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock();
- $calendarInfo = [
- '{http://owncloud.org/ns}owner-principal' => 'user1',
- 'principaluri' => 'user2',
- 'id' => 666,
- 'uri' => 'default'
- ];
- $c = new Calendar($backend, $calendarInfo);
-
- if ($throws) {
- $this->setExpectedException('\Sabre\DAV\Exception\Forbidden');
- }
- $c->propPatch(new PropPatch($mutations));
- if (!$throws) {
- $this->assertTrue(true);
- }
- }
-
- /**
- * @dataProvider providesReadOnlyInfo
- */
- public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet) {
- /** @var \PHPUnit_Framework_MockObject_MockObject | CalDavBackend $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock();
- $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1);
- $calendarInfo = [
- 'principaluri' => 'user2',
- 'id' => 666,
- 'uri' => 'default'
- ];
- if (!is_null($readOnlyValue)) {
- $calendarInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue;
- }
- if ($hasOwnerSet) {
- $calendarInfo['{http://owncloud.org/ns}owner-principal'] = 'user1';
- }
- $c = new Calendar($backend, $calendarInfo);
- $acl = $c->getACL();
- $childAcl = $c->getChildACL();
-
- $expectedAcl = [[
- 'privilege' => '{DAV:}read',
- 'principal' => $hasOwnerSet ? 'user1' : 'user2',
- 'protected' => true
- ], [
- 'privilege' => '{DAV:}write',
- 'principal' => $hasOwnerSet ? 'user1' : 'user2',
- 'protected' => true
- ]];
- if ($hasOwnerSet) {
- $expectedAcl[] = [
- 'privilege' => '{DAV:}read',
- 'principal' => 'user2',
- 'protected' => true
- ];
- if ($expectsWrite) {
- $expectedAcl[] = [
- 'privilege' => '{DAV:}write',
- 'principal' => 'user2',
- 'protected' => true
- ];
- }
- }
- $this->assertEquals($expectedAcl, $acl);
- $this->assertEquals($expectedAcl, $childAcl);
- }
-
- public function providesReadOnlyInfo() {
- return [
- 'read-only property not set' => [true, null, true],
- 'read-only property is false' => [true, false, true],
- 'read-only property is true' => [false, true, true],
- 'read-only property not set and no owner' => [true, null, false],
- 'read-only property is false and no owner' => [true, false, false],
- 'read-only property is true and no owner' => [false, true, false],
- ];
- }
-}
diff --git a/apps/dav/tests/unit/caldav/schedule/imipplugintest.php b/apps/dav/tests/unit/caldav/schedule/imipplugintest.php
deleted file mode 100644
index fcbf4fde04c..00000000000
--- a/apps/dav/tests/unit/caldav/schedule/imipplugintest.php
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\CalDAV\Schedule;
-
-use OC\Mail\Mailer;
-use OCA\DAV\CalDAV\Schedule\IMipPlugin;
-use OCP\ILogger;
-use Sabre\VObject\Component\VCalendar;
-use Sabre\VObject\ITip\Message;
-use Test\TestCase;
-
-class IMipPluginTest extends TestCase {
-
- public function testDelivery() {
- $mailMessage = new \OC\Mail\Message(new \Swift_Message());
- /** @var Mailer | \PHPUnit_Framework_MockObject_MockObject $mailer */
- $mailer = $this->getMockBuilder('OC\Mail\Mailer')->disableOriginalConstructor()->getMock();
- $mailer->method('createMessage')->willReturn($mailMessage);
- $mailer->expects($this->once())->method('send');
- /** @var ILogger | \PHPUnit_Framework_MockObject_MockObject $logger */
- $logger = $this->getMockBuilder('OC\Log')->disableOriginalConstructor()->getMock();
-
- $plugin = new IMipPlugin($mailer, $logger);
- $message = new Message();
- $message->method = 'REQUEST';
- $message->message = new VCalendar();
- $message->message->add('VEVENT', [
- 'UID' => $message->uid,
- 'SEQUENCE' => $message->sequence,
- 'SUMMARY' => 'Fellowship meeting',
- ]);
- $message->sender = 'mailto:gandalf@wiz.ard';
- $message->recipient = 'mailto:frodo@hobb.it';
-
- $plugin->schedule($message);
- $this->assertEquals('1.1', $message->getScheduleStatus());
- $this->assertEquals('Fellowship meeting', $mailMessage->getSubject());
- $this->assertEquals(['frodo@hobb.it' => null], $mailMessage->getTo());
- $this->assertEquals(['gandalf@wiz.ard' => null], $mailMessage->getReplyTo());
- $this->assertEquals('text/calendar; charset=UTF-8; method=REQUEST', $mailMessage->getSwiftMessage()->getContentType());
- }
-
- public function testFailedDelivery() {
- $mailMessage = new \OC\Mail\Message(new \Swift_Message());
- /** @var Mailer | \PHPUnit_Framework_MockObject_MockObject $mailer */
- $mailer = $this->getMockBuilder('OC\Mail\Mailer')->disableOriginalConstructor()->getMock();
- $mailer->method('createMessage')->willReturn($mailMessage);
- $mailer->method('send')->willThrowException(new \Exception());
- /** @var ILogger | \PHPUnit_Framework_MockObject_MockObject $logger */
- $logger = $this->getMockBuilder('OC\Log')->disableOriginalConstructor()->getMock();
-
- $plugin = new IMipPlugin($mailer, $logger);
- $message = new Message();
- $message->method = 'REQUEST';
- $message->message = new VCalendar();
- $message->message->add('VEVENT', [
- 'UID' => $message->uid,
- 'SEQUENCE' => $message->sequence,
- 'SUMMARY' => 'Fellowship meeting',
- ]);
- $message->sender = 'mailto:gandalf@wiz.ard';
- $message->recipient = 'mailto:frodo@hobb.it';
-
- $plugin->schedule($message);
- $this->assertEquals('5.0', $message->getScheduleStatus());
- $this->assertEquals('Fellowship meeting', $mailMessage->getSubject());
- $this->assertEquals(['frodo@hobb.it' => null], $mailMessage->getTo());
- $this->assertEquals(['gandalf@wiz.ard' => null], $mailMessage->getReplyTo());
- $this->assertEquals('text/calendar; charset=UTF-8; method=REQUEST', $mailMessage->getSwiftMessage()->getContentType());
- }
-
-}
diff --git a/apps/dav/tests/unit/carddav/addressbookimpltest.php b/apps/dav/tests/unit/carddav/addressbookimpltest.php
deleted file mode 100644
index ba537a631be..00000000000
--- a/apps/dav/tests/unit/carddav/addressbookimpltest.php
+++ /dev/null
@@ -1,288 +0,0 @@
-<?php
-/**
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-
-namespace OCA\DAV\Tests\Unit\CardDAV;
-
-
-use OCA\DAV\CardDAV\AddressBook;
-use OCA\DAV\CardDAV\AddressBookImpl;
-use OCA\DAV\CardDAV\CardDavBackend;
-use Sabre\VObject\Component\VCard;
-use Sabre\VObject\Property\Text;
-use Test\TestCase;
-
-class AddressBookImplTest extends TestCase {
-
- /** @var AddressBookImpl */
- private $addressBookImpl;
-
- /** @var array */
- private $addressBookInfo;
-
- /** @var AddressBook | \PHPUnit_Framework_MockObject_MockObject */
- private $addressBook;
-
- /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject */
- private $backend;
-
- /** @var VCard | \PHPUnit_Framework_MockObject_MockObject */
- private $vCard;
-
- public function setUp() {
- parent::setUp();
-
- $this->addressBookInfo = [
- 'id' => 42,
- '{DAV:}displayname' => 'display name'
- ];
- $this->addressBook = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBook')
- ->disableOriginalConstructor()->getMock();
- $this->backend = $this->getMockBuilder('\OCA\DAV\CardDAV\CardDavBackend')
- ->disableOriginalConstructor()->getMock();
- $this->vCard = $this->getMock('Sabre\VObject\Component\VCard');
-
- $this->addressBookImpl = new AddressBookImpl(
- $this->addressBook,
- $this->addressBookInfo,
- $this->backend
- );
- }
-
- public function testGetKey() {
- $this->assertSame($this->addressBookInfo['id'],
- $this->addressBookImpl->getKey());
- }
-
- public function testGetDisplayName() {
- $this->assertSame($this->addressBookInfo['{DAV:}displayname'],
- $this->addressBookImpl->getDisplayName());
- }
-
- public function testSearch() {
-
- /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */
- $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl')
- ->setConstructorArgs(
- [
- $this->addressBook,
- $this->addressBookInfo,
- $this->backend
- ]
- )
- ->setMethods(['vCard2Array', 'readCard'])
- ->getMock();
-
- $pattern = 'pattern';
- $searchProperties = 'properties';
-
- $this->backend->expects($this->once())->method('search')
- ->with($this->addressBookInfo['id'], $pattern, $searchProperties)
- ->willReturn(
- [
- 'cardData1',
- 'cardData2'
- ]
- );
-
- $addressBookImpl->expects($this->exactly(2))->method('readCard')
- ->willReturn($this->vCard);
- $addressBookImpl->expects($this->exactly(2))->method('vCard2Array')
- ->with($this->vCard)->willReturn('vCard');
-
- $result = $addressBookImpl->search($pattern, $searchProperties, []);
- $this->assertTrue((is_array($result)));
- $this->assertSame(2, count($result));
- }
-
- /**
- * @dataProvider dataTestCreate
- *
- * @param array $properties
- */
- public function testCreate($properties) {
-
- $uid = 'uid';
-
- /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */
- $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl')
- ->setConstructorArgs(
- [
- $this->addressBook,
- $this->addressBookInfo,
- $this->backend
- ]
- )
- ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard'])
- ->getMock();
-
- $addressBookImpl->expects($this->once())->method('createUid')
- ->willReturn($uid);
- $addressBookImpl->expects($this->once())->method('createEmptyVCard')
- ->with($uid)->willReturn($this->vCard);
- $this->vCard->expects($this->exactly(count($properties)))
- ->method('createProperty');
- $this->backend->expects($this->once())->method('createCard');
- $this->backend->expects($this->never())->method('updateCard');
- $this->backend->expects($this->never())->method('getCard');
- $addressBookImpl->expects($this->once())->method('vCard2Array')
- ->with($this->vCard)->willReturn(true);
-
- $this->assertTrue($addressBookImpl->createOrUpdate($properties));
- }
-
- public function dataTestCreate() {
- return [
- [[]],
- [['FN' => 'John Doe']]
- ];
- }
-
- public function testUpdate() {
-
- $uid = 'uid';
- $properties = ['UID' => $uid, 'FN' => 'John Doe'];
-
- /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */
- $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl')
- ->setConstructorArgs(
- [
- $this->addressBook,
- $this->addressBookInfo,
- $this->backend
- ]
- )
- ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard'])
- ->getMock();
-
- $addressBookImpl->expects($this->never())->method('createUid');
- $addressBookImpl->expects($this->never())->method('createEmptyVCard');
- $this->backend->expects($this->once())->method('getCard')
- ->with($this->addressBookInfo['id'], $uid . '.vcf')
- ->willReturn(['carddata' => 'data']);
- $addressBookImpl->expects($this->once())->method('readCard')
- ->with('data')->willReturn($this->vCard);
- $this->vCard->expects($this->exactly(count($properties)))
- ->method('createProperty');
- $this->backend->expects($this->never())->method('createCard');
- $this->backend->expects($this->once())->method('updateCard');
- $addressBookImpl->expects($this->once())->method('vCard2Array')
- ->with($this->vCard)->willReturn(true);
-
- $this->assertTrue($addressBookImpl->createOrUpdate($properties));
- }
-
- /**
- * @dataProvider dataTestGetPermissions
- *
- * @param array $permissions
- * @param int $expected
- */
- public function testGetPermissions($permissions, $expected) {
- $this->addressBook->expects($this->once())->method('getACL')
- ->willReturn($permissions);
-
- $this->assertSame($expected,
- $this->addressBookImpl->getPermissions()
- );
- }
-
- public function dataTestGetPermissions() {
- return [
- [[], 0],
- [[['privilege' => '{DAV:}read']], 1],
- [[['privilege' => '{DAV:}write']], 6],
- [[['privilege' => '{DAV:}all']], 31],
- [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write']], 7],
- [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}all']], 31],
- [[['privilege' => '{DAV:}all'],['privilege' => '{DAV:}write']], 31],
- [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write'],['privilege' => '{DAV:}all']], 31],
- [[['privilege' => '{DAV:}all'],['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write']], 31],
- ];
- }
-
- public function testDelete() {
- $cardId = 1;
- $cardUri = 'cardUri';
- $this->backend->expects($this->once())->method('getCardUri')
- ->with($cardId)->willReturn($cardUri);
- $this->backend->expects($this->once())->method('deleteCard')
- ->with($this->addressBookInfo['id'], $cardUri)
- ->willReturn(true);
-
- $this->assertTrue($this->addressBookImpl->delete($cardId));
- }
-
- public function testReadCard() {
- $vCard = new VCard();
- $vCard->add(new Text($vCard, 'UID', 'uid'));
- $vCardSerialized = $vCard->serialize();
-
- $result = $this->invokePrivate($this->addressBookImpl, 'readCard', [$vCardSerialized]);
- $resultSerialized = $result->serialize();
-
- $this->assertSame($vCardSerialized, $resultSerialized);
- }
-
- public function testCreateUid() {
- /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */
- $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl')
- ->setConstructorArgs(
- [
- $this->addressBook,
- $this->addressBookInfo,
- $this->backend
- ]
- )
- ->setMethods(['getUid'])
- ->getMock();
-
- $addressBookImpl->expects($this->at(0))->method('getUid')->willReturn('uid0');
- $addressBookImpl->expects($this->at(1))->method('getUid')->willReturn('uid1');
-
- // simulate that 'uid0' already exists, so the second uid will be returned
- $this->backend->expects($this->exactly(2))->method('getContact')
- ->willReturnCallback(
- function($id, $uid) {
- return ($uid === 'uid0.vcf');
- }
- );
-
- $this->assertSame('uid1',
- $this->invokePrivate($addressBookImpl, 'createUid', [])
- );
-
- }
-
- public function testCreateEmptyVCard() {
- $uid = 'uid';
- $expectedVCard = new VCard();
- $expectedVCard->add(new Text($expectedVCard, 'UID', $uid));
- $expectedVCardSerialized = $expectedVCard->serialize();
-
- $result = $this->invokePrivate($this->addressBookImpl, 'createEmptyVCard', [$uid]);
- $resultSerialized = $result->serialize();
-
- $this->assertSame($expectedVCardSerialized, $resultSerialized);
- }
-
-}
diff --git a/apps/dav/tests/unit/carddav/addressbooktest.php b/apps/dav/tests/unit/carddav/addressbooktest.php
deleted file mode 100644
index c5cf7e5f7ba..00000000000
--- a/apps/dav/tests/unit/carddav/addressbooktest.php
+++ /dev/null
@@ -1,139 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\CardDAV;
-
-use OCA\DAV\CardDAV\AddressBook;
-use OCA\DAV\CardDAV\CardDavBackend;
-use Sabre\DAV\PropPatch;
-use Test\TestCase;
-
-class AddressBookTest extends TestCase {
-
- public function testDelete() {
- /** @var \PHPUnit_Framework_MockObject_MockObject | CardDavBackend $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock();
- $backend->expects($this->once())->method('updateShares');
- $backend->expects($this->any())->method('getShares')->willReturn([
- ['href' => 'principal:user2']
- ]);
- $calendarInfo = [
- '{http://owncloud.org/ns}owner-principal' => 'user1',
- 'principaluri' => 'user2',
- 'id' => 666
- ];
- $c = new AddressBook($backend, $calendarInfo);
- $c->delete();
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testDeleteFromGroup() {
- /** @var \PHPUnit_Framework_MockObject_MockObject | CardDavBackend $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock();
- $backend->expects($this->never())->method('updateShares');
- $backend->expects($this->any())->method('getShares')->willReturn([
- ['href' => 'principal:group2']
- ]);
- $calendarInfo = [
- '{http://owncloud.org/ns}owner-principal' => 'user1',
- 'principaluri' => 'user2',
- 'id' => 666
- ];
- $c = new AddressBook($backend, $calendarInfo);
- $c->delete();
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testPropPatch() {
- /** @var \PHPUnit_Framework_MockObject_MockObject | CardDavBackend $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock();
- $calendarInfo = [
- '{http://owncloud.org/ns}owner-principal' => 'user1',
- 'principaluri' => 'user2',
- 'id' => 666
- ];
- $c = new AddressBook($backend, $calendarInfo);
- $c->propPatch(new PropPatch([]));
- }
-
- /**
- * @dataProvider providesReadOnlyInfo
- */
- public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet) {
- /** @var \PHPUnit_Framework_MockObject_MockObject | CardDavBackend $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock();
- $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1);
- $calendarInfo = [
- 'principaluri' => 'user2',
- 'id' => 666,
- 'uri' => 'default'
- ];
- if (!is_null($readOnlyValue)) {
- $calendarInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue;
- }
- if ($hasOwnerSet) {
- $calendarInfo['{http://owncloud.org/ns}owner-principal'] = 'user1';
- }
- $c = new AddressBook($backend, $calendarInfo);
- $acl = $c->getACL();
- $childAcl = $c->getChildACL();
-
- $expectedAcl = [[
- 'privilege' => '{DAV:}read',
- 'principal' => $hasOwnerSet ? 'user1' : 'user2',
- 'protected' => true
- ], [
- 'privilege' => '{DAV:}write',
- 'principal' => $hasOwnerSet ? 'user1' : 'user2',
- 'protected' => true
- ]];
- if ($hasOwnerSet) {
- $expectedAcl[] = [
- 'privilege' => '{DAV:}read',
- 'principal' => 'user2',
- 'protected' => true
- ];
- if ($expectsWrite) {
- $expectedAcl[] = [
- 'privilege' => '{DAV:}write',
- 'principal' => 'user2',
- 'protected' => true
- ];
- }
- }
- $this->assertEquals($expectedAcl, $acl);
- $this->assertEquals($expectedAcl, $childAcl);
- }
-
- public function providesReadOnlyInfo() {
- return [
- 'read-only property not set' => [true, null, true],
- 'read-only property is false' => [true, false, true],
- 'read-only property is true' => [false, true, true],
- 'read-only property not set and no owner' => [true, null, false],
- 'read-only property is false and no owner' => [true, false, false],
- 'read-only property is true and no owner' => [false, true, false],
- ];
- }}
diff --git a/apps/dav/tests/unit/carddav/birthdayservicetest.php b/apps/dav/tests/unit/carddav/birthdayservicetest.php
deleted file mode 100644
index 2efb3c09aea..00000000000
--- a/apps/dav/tests/unit/carddav/birthdayservicetest.php
+++ /dev/null
@@ -1,171 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\CardDAV;
-
-use OCA\DAV\CalDAV\BirthdayService;
-use OCA\DAV\CalDAV\CalDavBackend;
-use OCA\DAV\CardDAV\CardDavBackend;
-use Sabre\VObject\Component\VCalendar;
-use Sabre\VObject\Reader;
-use Test\TestCase;
-
-class BirthdayServiceTest extends TestCase {
-
- /** @var BirthdayService */
- private $service;
- /** @var CalDavBackend | \PHPUnit_Framework_MockObject_MockObject */
- private $calDav;
- /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject */
- private $cardDav;
-
- public function setUp() {
- parent::setUp();
-
- $this->calDav = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock();
- $this->cardDav = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock();
-
- $this->service = new BirthdayService($this->calDav, $this->cardDav);
- }
-
- /**
- * @dataProvider providesVCards
- * @param boolean $nullExpected
- * @param string | null $data
- */
- public function testBuildBirthdayFromContact($nullExpected, $data) {
- $cal = $this->service->buildBirthdayFromContact($data);
- if ($nullExpected) {
- $this->assertNull($cal);
- } else {
- $this->assertInstanceOf('Sabre\VObject\Component\VCalendar', $cal);
- $this->assertTrue(isset($cal->VEVENT));
- $this->assertEquals('FREQ=YEARLY', $cal->VEVENT->RRULE->getValue());
- $this->assertEquals('12345 (*1900)', $cal->VEVENT->SUMMARY->getValue());
- $this->assertEquals('TRANSPARENT', $cal->VEVENT->TRANSP->getValue());
- }
- }
-
- public function testOnCardDeleted() {
- $this->cardDav->expects($this->once())->method('getAddressBookById')
- ->with(666)
- ->willReturn([
- 'principaluri' => 'principals/users/user01',
- 'uri' => 'default'
- ]);
- $this->calDav->expects($this->once())->method('getCalendarByUri')
- ->with('principals/users/user01', 'contact_birthdays')
- ->willReturn([
- 'id' => 1234
- ]);
- $this->calDav->expects($this->once())->method('deleteCalendarObject')->with(1234, 'default-gump.vcf.ics');
-
- $this->service->onCardDeleted(666, 'gump.vcf');
- }
-
- /**
- * @dataProvider providesCardChanges
- */
- public function testOnCardChanged($expectedOp) {
- $this->cardDav->expects($this->once())->method('getAddressBookById')
- ->with(666)
- ->willReturn([
- 'principaluri' => 'principals/users/user01',
- 'uri' => 'default'
- ]);
- $this->calDav->expects($this->once())->method('getCalendarByUri')
- ->with('principals/users/user01', 'contact_birthdays')
- ->willReturn([
- 'id' => 1234
- ]);
-
- /** @var BirthdayService | \PHPUnit_Framework_MockObject_MockObject $service */
- $service = $this->getMock('\OCA\DAV\CalDAV\BirthdayService',
- ['buildBirthdayFromContact', 'birthdayEvenChanged'], [$this->calDav, $this->cardDav]);
-
- if ($expectedOp === 'delete') {
- $this->calDav->expects($this->once())->method('getCalendarObject')->willReturn('');
- $service->expects($this->once())->method('buildBirthdayFromContact')->willReturn(null);
- $this->calDav->expects($this->once())->method('deleteCalendarObject')->with(1234, 'default-gump.vcf.ics');
- }
- if ($expectedOp === 'create') {
- $service->expects($this->once())->method('buildBirthdayFromContact')->willReturn(new VCalendar());
- $this->calDav->expects($this->once())->method('createCalendarObject')->with(1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nEND:VCALENDAR\r\n");
- }
- if ($expectedOp === 'update') {
- $service->expects($this->once())->method('buildBirthdayFromContact')->willReturn(new VCalendar());
- $service->expects($this->once())->method('birthdayEvenChanged')->willReturn(true);
- $this->calDav->expects($this->once())->method('getCalendarObject')->willReturn([
- 'calendardata' => '']);
- $this->calDav->expects($this->once())->method('updateCalendarObject')->with(1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nEND:VCALENDAR\r\n");
- }
-
- $service->onCardChanged(666, 'gump.vcf', '');
- }
-
- /**
- * @dataProvider providesBirthday
- * @param $expected
- * @param $old
- * @param $new
- */
- public function testBirthdayEvenChanged($expected, $old, $new) {
- $new = Reader::read($new);
- $this->assertEquals($expected, $this->service->birthdayEvenChanged($old, $new));
- }
-
- public function providesBirthday() {
- return [
- [true,
- '',
- "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
- [false,
- "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
- "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
- [true,
- "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:4567's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
- "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
- [true,
- "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
- "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000102\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"]
- ];
- }
-
- public function providesCardChanges(){
- return[
- ['delete'],
- ['create'],
- ['update']
- ];
- }
-
- public function providesVCards() {
- return [
- [true, null],
- [true, ''],
- [true, 'yasfewf'],
- [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar"],
- [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:\r\nEND:VCARD\r\n", "Dr. Foo Bar"],
- [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:someday\r\nEND:VCARD\r\n", "Dr. Foo Bar"],
- [false, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:1900-01-01\r\nEND:VCARD\r\n", "Dr. Foo Bar"],
- ];
- }
-}
diff --git a/apps/dav/tests/unit/carddav/carddavbackendtest.php b/apps/dav/tests/unit/carddav/carddavbackendtest.php
deleted file mode 100644
index 1ee09260c88..00000000000
--- a/apps/dav/tests/unit/carddav/carddavbackendtest.php
+++ /dev/null
@@ -1,631 +0,0 @@
-<?php
-/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\CardDAV;
-
-use InvalidArgumentException;
-use OCA\DAV\CardDAV\AddressBook;
-use OCA\DAV\CardDAV\CardDavBackend;
-use OCA\DAV\Connector\Sabre\Principal;
-use OCP\DB\QueryBuilder\IQueryBuilder;
-use OCP\IDBConnection;
-use Sabre\DAV\PropPatch;
-use Sabre\VObject\Component\VCard;
-use Sabre\VObject\Property\Text;
-use Test\TestCase;
-
-/**
- * Class CardDavBackendTest
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\Unit\CardDAV
- */
-class CardDavBackendTest extends TestCase {
-
- /** @var CardDavBackend */
- private $backend;
-
- /** @var Principal | \PHPUnit_Framework_MockObject_MockObject */
- private $principal;
-
- /** @var IDBConnection */
- private $db;
-
- /** @var string */
- private $dbCardsTable = 'cards';
-
- /** @var string */
- private $dbCardsPropertiesTable = 'cards_properties';
-
- const UNIT_TEST_USER = 'principals/users/carddav-unit-test';
- const UNIT_TEST_USER1 = 'principals/users/carddav-unit-test1';
- const UNIT_TEST_GROUP = 'principals/groups/carddav-unit-test-group';
-
- public function setUp() {
- parent::setUp();
-
- $this->principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal')
- ->disableOriginalConstructor()
- ->setMethods(['getPrincipalByPath', 'getGroupMembership'])
- ->getMock();
- $this->principal->method('getPrincipalByPath')
- ->willReturn([
- 'uri' => 'principals/best-friend'
- ]);
- $this->principal->method('getGroupMembership')
- ->withAnyParameters()
- ->willReturn([self::UNIT_TEST_GROUP]);
-
- $this->db = \OC::$server->getDatabaseConnection();
-
- $this->backend = new CardDavBackend($this->db, $this->principal, null);
-
- // start every test with a empty cards_properties and cards table
- $query = $this->db->getQueryBuilder();
- $query->delete('cards_properties')->execute();
- $query = $this->db->getQueryBuilder();
- $query->delete('cards')->execute();
-
-
- $this->tearDown();
- }
-
- public function tearDown() {
- parent::tearDown();
-
- if (is_null($this->backend)) {
- return;
- }
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- foreach ($books as $book) {
- $this->backend->deleteAddressBook($book['id']);
- }
- }
-
- public function testAddressBookOperations() {
-
- // create a new address book
- $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
-
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($books));
- $this->assertEquals('Example', $books[0]['{DAV:}displayname']);
-
- // update it's display name
- $patch = new PropPatch([
- '{DAV:}displayname' => 'Unit test',
- '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Addressbook used for unit testing'
- ]);
- $this->backend->updateAddressBook($books[0]['id'], $patch);
- $patch->commit();
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($books));
- $this->assertEquals('Unit test', $books[0]['{DAV:}displayname']);
- $this->assertEquals('Addressbook used for unit testing', $books[0]['{urn:ietf:params:xml:ns:carddav}addressbook-description']);
-
- // delete the address book
- $this->backend->deleteAddressBook($books[0]['id']);
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- $this->assertEquals(0, count($books));
- }
-
- public function testAddressBookSharing() {
-
- $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($books));
- $addressBook = new AddressBook($this->backend, $books[0]);
- $this->backend->updateShares($addressBook, [
- [
- 'href' => 'principal:' . self::UNIT_TEST_USER1,
- ],
- [
- 'href' => 'principal:' . self::UNIT_TEST_GROUP,
- ]
- ], []);
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER1);
- $this->assertEquals(1, count($books));
-
- // delete the address book
- $this->backend->deleteAddressBook($books[0]['id']);
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- $this->assertEquals(0, count($books));
- }
-
- public function testCardOperations() {
-
- /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')
- ->setConstructorArgs([$this->db, $this->principal, null])
- ->setMethods(['updateProperties', 'purgeProperties'])->getMock();
-
- // create a new address book
- $backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
- $books = $backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($books));
- $bookId = $books[0]['id'];
-
- $uri = $this->getUniqueID('card');
- // updateProperties is expected twice, once for createCard and once for updateCard
- $backend->expects($this->at(0))->method('updateProperties')->with($bookId, $uri, '');
- $backend->expects($this->at(1))->method('updateProperties')->with($bookId, $uri, '***');
- // create a card
- $backend->createCard($bookId, $uri, '');
-
- // get all the cards
- $cards = $backend->getCards($bookId);
- $this->assertEquals(1, count($cards));
- $this->assertEquals('', $cards[0]['carddata']);
-
- // get the cards
- $card = $backend->getCard($bookId, $uri);
- $this->assertNotNull($card);
- $this->assertArrayHasKey('id', $card);
- $this->assertArrayHasKey('uri', $card);
- $this->assertArrayHasKey('lastmodified', $card);
- $this->assertArrayHasKey('etag', $card);
- $this->assertArrayHasKey('size', $card);
- $this->assertEquals('', $card['carddata']);
-
- // update the card
- $backend->updateCard($bookId, $uri, '***');
- $card = $backend->getCard($bookId, $uri);
- $this->assertEquals('***', $card['carddata']);
-
- // delete the card
- $backend->expects($this->once())->method('purgeProperties')->with($bookId, $card['id']);
- $backend->deleteCard($bookId, $uri);
- $cards = $backend->getCards($bookId);
- $this->assertEquals(0, count($cards));
- }
-
- public function testMultiCard() {
-
- $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')
- ->setConstructorArgs([$this->db, $this->principal, null])
- ->setMethods(['updateProperties'])->getMock();
-
- // create a new address book
- $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($books));
- $bookId = $books[0]['id'];
-
- // create a card
- $uri0 = $this->getUniqueID('card');
- $this->backend->createCard($bookId, $uri0, '');
- $uri1 = $this->getUniqueID('card');
- $this->backend->createCard($bookId, $uri1, '');
- $uri2 = $this->getUniqueID('card');
- $this->backend->createCard($bookId, $uri2, '');
-
- // get all the cards
- $cards = $this->backend->getCards($bookId);
- $this->assertEquals(3, count($cards));
- $this->assertEquals('', $cards[0]['carddata']);
- $this->assertEquals('', $cards[1]['carddata']);
- $this->assertEquals('', $cards[2]['carddata']);
-
- // get the cards
- $cards = $this->backend->getMultipleCards($bookId, [$uri1, $uri2]);
- $this->assertEquals(2, count($cards));
- foreach($cards as $card) {
- $this->assertArrayHasKey('id', $card);
- $this->assertArrayHasKey('uri', $card);
- $this->assertArrayHasKey('lastmodified', $card);
- $this->assertArrayHasKey('etag', $card);
- $this->assertArrayHasKey('size', $card);
- $this->assertEquals('', $card['carddata']);
- }
-
- // delete the card
- $this->backend->deleteCard($bookId, $uri0);
- $this->backend->deleteCard($bookId, $uri1);
- $this->backend->deleteCard($bookId, $uri2);
- $cards = $this->backend->getCards($bookId);
- $this->assertEquals(0, count($cards));
- }
-
- public function testDeleteWithoutCard() {
-
- $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')
- ->setConstructorArgs([$this->db, $this->principal, null])
- ->setMethods([
- 'getCardId',
- 'addChange',
- 'purgeProperties',
- 'updateProperties',
- ])
- ->getMock();
-
- // create a new address book
- $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($books));
-
- $bookId = $books[0]['id'];
- $uri = $this->getUniqueID('card');
-
- // create a new address book
- $this->backend->expects($this->once())
- ->method('getCardId')
- ->with($bookId, $uri)
- ->willThrowException(new \InvalidArgumentException());
- $this->backend->expects($this->exactly(2))
- ->method('addChange')
- ->withConsecutive(
- [$bookId, $uri, 1],
- [$bookId, $uri, 3]
- );
- $this->backend->expects($this->never())
- ->method('purgeProperties');
-
- // create a card
- $this->backend->createCard($bookId, $uri, '');
-
- // delete the card
- $this->assertTrue($this->backend->deleteCard($bookId, $uri));
- }
-
- public function testSyncSupport() {
-
- $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')
- ->setConstructorArgs([$this->db, $this->principal, null])
- ->setMethods(['updateProperties'])->getMock();
-
- // create a new address book
- $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($books));
- $bookId = $books[0]['id'];
-
- // fist call without synctoken
- $changes = $this->backend->getChangesForAddressBook($bookId, '', 1);
- $syncToken = $changes['syncToken'];
-
- // add a change
- $uri0 = $this->getUniqueID('card');
- $this->backend->createCard($bookId, $uri0, '');
-
- // look for changes
- $changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1);
- $this->assertEquals($uri0, $changes['added'][0]);
- }
-
- public function testSharing() {
- $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
- $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
- $this->assertEquals(1, count($books));
-
- $exampleBook = new AddressBook($this->backend, $books[0]);
- $this->backend->updateShares($exampleBook, [['href' => 'principal:principals/best-friend']], []);
-
- $shares = $this->backend->getShares($exampleBook->getResourceId());
- $this->assertEquals(1, count($shares));
-
- // adding the same sharee again has no effect
- $this->backend->updateShares($exampleBook, [['href' => 'principal:principals/best-friend']], []);
-
- $shares = $this->backend->getShares($exampleBook->getResourceId());
- $this->assertEquals(1, count($shares));
-
- $books = $this->backend->getAddressBooksForUser('principals/best-friend');
- $this->assertEquals(1, count($books));
-
- $this->backend->updateShares($exampleBook, [], ['principal:principals/best-friend']);
-
- $shares = $this->backend->getShares($exampleBook->getResourceId());
- $this->assertEquals(0, count($shares));
-
- $books = $this->backend->getAddressBooksForUser('principals/best-friend');
- $this->assertEquals(0, count($books));
- }
-
- public function testUpdateProperties() {
-
- $bookId = 42;
- $cardUri = 'card-uri';
- $cardId = 2;
-
- $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')
- ->setConstructorArgs([$this->db, $this->principal, null])
- ->setMethods(['getCardId'])->getMock();
-
- $backend->expects($this->any())->method('getCardId')->willReturn($cardId);
-
- // add properties for new vCard
- $vCard = new VCard();
- $vCard->add(new Text($vCard, 'UID', $cardUri));
- $vCard->add(new Text($vCard, 'FN', 'John Doe'));
- $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]);
-
- $query = $this->db->getQueryBuilder();
- $result = $query->select('*')->from('cards_properties')->execute()->fetchAll();
-
- $this->assertSame(2, count($result));
-
- $this->assertSame('UID', $result[0]['name']);
- $this->assertSame($cardUri, $result[0]['value']);
- $this->assertSame($bookId, (int)$result[0]['addressbookid']);
- $this->assertSame($cardId, (int)$result[0]['cardid']);
-
- $this->assertSame('FN', $result[1]['name']);
- $this->assertSame('John Doe', $result[1]['value']);
- $this->assertSame($bookId, (int)$result[1]['addressbookid']);
- $this->assertSame($cardId, (int)$result[1]['cardid']);
-
- // update properties for existing vCard
- $vCard = new VCard();
- $vCard->add(new Text($vCard, 'FN', 'John Doe'));
- $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]);
-
- $query = $this->db->getQueryBuilder();
- $result = $query->select('*')->from('cards_properties')->execute()->fetchAll();
-
- $this->assertSame(1, count($result));
-
- $this->assertSame('FN', $result[0]['name']);
- $this->assertSame('John Doe', $result[0]['value']);
- $this->assertSame($bookId, (int)$result[0]['addressbookid']);
- $this->assertSame($cardId, (int)$result[0]['cardid']);
- }
-
- public function testPurgeProperties() {
-
- $query = $this->db->getQueryBuilder();
- $query->insert('cards_properties')
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(1),
- 'cardid' => $query->createNamedParameter(1),
- 'name' => $query->createNamedParameter('name1'),
- 'value' => $query->createNamedParameter('value1'),
- 'preferred' => $query->createNamedParameter(0)
- ]
- );
- $query->execute();
-
- $query = $this->db->getQueryBuilder();
- $query->insert('cards_properties')
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(1),
- 'cardid' => $query->createNamedParameter(2),
- 'name' => $query->createNamedParameter('name2'),
- 'value' => $query->createNamedParameter('value2'),
- 'preferred' => $query->createNamedParameter(0)
- ]
- );
- $query->execute();
-
- $this->invokePrivate($this->backend, 'purgeProperties', [1, 1]);
-
- $query = $this->db->getQueryBuilder();
- $result = $query->select('*')->from('cards_properties')->execute()->fetchAll();
- $this->assertSame(1, count($result));
- $this->assertSame(1 ,(int)$result[0]['addressbookid']);
- $this->assertSame(2 ,(int)$result[0]['cardid']);
-
- }
-
- public function testGetCardId() {
- $query = $this->db->getQueryBuilder();
-
- $query->insert('cards')
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(1),
- 'carddata' => $query->createNamedParameter(''),
- 'uri' => $query->createNamedParameter('uri'),
- 'lastmodified' => $query->createNamedParameter(4738743),
- 'etag' => $query->createNamedParameter('etag'),
- 'size' => $query->createNamedParameter(120)
- ]
- );
- $query->execute();
- $id = $query->getLastInsertId();
-
- $this->assertSame($id,
- $this->invokePrivate($this->backend, 'getCardId', [1, 'uri']));
- }
-
- /**
- * @expectedException InvalidArgumentException
- */
- public function testGetCardIdFailed() {
- $this->invokePrivate($this->backend, 'getCardId', [1, 'uri']);
- }
-
- /**
- * @dataProvider dataTestSearch
- *
- * @param string $pattern
- * @param array $properties
- * @param array $expected
- */
- public function testSearch($pattern, $properties, $expected) {
- /** @var VCard $vCards */
- $vCards = [];
- $vCards[0] = new VCard();
- $vCards[0]->add(new Text($vCards[0], 'UID', 'uid'));
- $vCards[0]->add(new Text($vCards[0], 'FN', 'John Doe'));
- $vCards[0]->add(new Text($vCards[0], 'CLOUD', 'john@owncloud.org'));
- $vCards[1] = new VCard();
- $vCards[1]->add(new Text($vCards[1], 'UID', 'uid'));
- $vCards[1]->add(new Text($vCards[1], 'FN', 'John M. Doe'));
-
- $vCardIds = [];
- $query = $this->db->getQueryBuilder();
- for($i=0; $i<2; $i++) {
- $query->insert($this->dbCardsTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(0),
- 'carddata' => $query->createNamedParameter($vCards[$i]->serialize(), IQueryBuilder::PARAM_LOB),
- 'uri' => $query->createNamedParameter('uri' . $i),
- 'lastmodified' => $query->createNamedParameter(time()),
- 'etag' => $query->createNamedParameter('etag' . $i),
- 'size' => $query->createNamedParameter(120),
- ]
- );
- $query->execute();
- $vCardIds[] = $query->getLastInsertId();
- }
-
- $query->insert($this->dbCardsPropertiesTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(0),
- 'cardid' => $query->createNamedParameter($vCardIds[0]),
- 'name' => $query->createNamedParameter('FN'),
- 'value' => $query->createNamedParameter('John Doe'),
- 'preferred' => $query->createNamedParameter(0)
- ]
- );
- $query->execute();
- $query->insert($this->dbCardsPropertiesTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(0),
- 'cardid' => $query->createNamedParameter($vCardIds[0]),
- 'name' => $query->createNamedParameter('CLOUD'),
- 'value' => $query->createNamedParameter('John@owncloud.org'),
- 'preferred' => $query->createNamedParameter(0)
- ]
- );
- $query->execute();
- $query->insert($this->dbCardsPropertiesTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(0),
- 'cardid' => $query->createNamedParameter($vCardIds[1]),
- 'name' => $query->createNamedParameter('FN'),
- 'value' => $query->createNamedParameter('John M. Doe'),
- 'preferred' => $query->createNamedParameter(0)
- ]
- );
- $query->execute();
-
- $result = $this->backend->search(0, $pattern, $properties);
-
- // check result
- $this->assertSame(count($expected), count($result));
- $found = [];
- foreach ($result as $r) {
- foreach ($expected as $exp) {
- if (strpos($r, $exp) > 0) {
- $found[$exp] = true;
- break;
- }
- }
- }
-
- $this->assertSame(count($expected), count($found));
- }
-
- public function dataTestSearch() {
- return [
- ['John', ['FN'], ['John Doe', 'John M. Doe']],
- ['M. Doe', ['FN'], ['John M. Doe']],
- ['Do', ['FN'], ['John Doe', 'John M. Doe']],
- 'check if duplicates are handled correctly' => ['John', ['FN', 'CLOUD'], ['John Doe', 'John M. Doe']],
- 'case insensitive' => ['john', ['FN'], ['John Doe', 'John M. Doe']]
- ];
- }
-
- public function testGetCardUri() {
- $query = $this->db->getQueryBuilder();
- $query->insert($this->dbCardsTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(1),
- 'carddata' => $query->createNamedParameter('carddata', IQueryBuilder::PARAM_LOB),
- 'uri' => $query->createNamedParameter('uri'),
- 'lastmodified' => $query->createNamedParameter(5489543),
- 'etag' => $query->createNamedParameter('etag'),
- 'size' => $query->createNamedParameter(120),
- ]
- );
- $query->execute();
-
- $id = $query->getLastInsertId();
-
- $this->assertSame('uri', $this->backend->getCardUri($id));
- }
-
- /**
- * @expectedException InvalidArgumentException
- */
- public function testGetCardUriFailed() {
- $this->backend->getCardUri(1);
- }
-
- public function testGetContact() {
- $query = $this->db->getQueryBuilder();
- for($i=0; $i<2; $i++) {
- $query->insert($this->dbCardsTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter($i),
- 'carddata' => $query->createNamedParameter('carddata' . $i, IQueryBuilder::PARAM_LOB),
- 'uri' => $query->createNamedParameter('uri' . $i),
- 'lastmodified' => $query->createNamedParameter(5489543),
- 'etag' => $query->createNamedParameter('etag' . $i),
- 'size' => $query->createNamedParameter(120),
- ]
- );
- $query->execute();
- }
-
- $result = $this->backend->getContact(0, 'uri0');
- $this->assertSame(7, count($result));
- $this->assertSame(0, (int)$result['addressbookid']);
- $this->assertSame('uri0', $result['uri']);
- $this->assertSame(5489543, (int)$result['lastmodified']);
- $this->assertSame('etag0', $result['etag']);
- $this->assertSame(120, (int)$result['size']);
- }
-
- public function testGetContactFail() {
- $this->assertEmpty($this->backend->getContact(0, 'uri'));
- }
-
- public function testCollectCardProperties() {
- $query = $this->db->getQueryBuilder();
- $query->insert($this->dbCardsPropertiesTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(666),
- 'cardid' => $query->createNamedParameter(777),
- 'name' => $query->createNamedParameter('FN'),
- 'value' => $query->createNamedParameter('John Doe'),
- 'preferred' => $query->createNamedParameter(0)
- ]
- )
- ->execute();
-
- $result = $this->backend->collectCardProperties(666, 'FN');
- $this->assertEquals(['John Doe'], $result);
- }
-}
diff --git a/apps/dav/tests/unit/carddav/contactsmanagertest.php b/apps/dav/tests/unit/carddav/contactsmanagertest.php
deleted file mode 100644
index 5a384550df5..00000000000
--- a/apps/dav/tests/unit/carddav/contactsmanagertest.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\CardDAV;
-
-use OCA\DAV\CardDAV\CardDavBackend;
-use OCA\DAV\CardDAV\ContactsManager;
-use OCP\Contacts\IManager;
-use Test\TestCase;
-
-class ContactsManagerTest extends TestCase {
- public function test() {
- /** @var IManager | \PHPUnit_Framework_MockObject_MockObject $cm */
- $cm = $this->getMockBuilder('OCP\Contacts\IManager')->disableOriginalConstructor()->getMock();
- $cm->expects($this->exactly(2))->method('registerAddressBook');
- /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backEnd */
- $backEnd = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock();
- $backEnd->method('getAddressBooksForUser')->willReturn([
- []
- ]);
-
- $app = new ContactsManager($backEnd);
- $app->setupContactsProvider($cm, 'user01');
- }
-}
diff --git a/apps/dav/tests/unit/carddav/convertertest.php b/apps/dav/tests/unit/carddav/convertertest.php
deleted file mode 100644
index ba71b75686a..00000000000
--- a/apps/dav/tests/unit/carddav/convertertest.php
+++ /dev/null
@@ -1,137 +0,0 @@
-<?php
-/**
- * @author Roeland Jago Douma <rullzer@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit;
-
-use OCA\DAV\CardDAV\Converter;
-use Test\TestCase;
-
-class ConverterTests extends TestCase {
-
- /**
- * @dataProvider providesNewUsers
- */
- public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) {
- $user = $this->getUserMock($displayName, $eMailAddress, $cloudId);
-
- $converter = new Converter();
- $vCard = $converter->createCardFromUser($user);
- $cardData = $vCard->serialize();
-
- $this->assertEquals($expectedVCard, $cardData);
- }
-
- public function providesNewUsers() {
- return [
- ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n"],
- ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar"],
- ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nEMAIL;TYPE=OTHER:foo@bar.net\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar", "foo@bar.net"],
- ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nCLOUD:foo@bar.net\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar", null, "foo@bar.net"],
- ];
- }
-
- /**
- * @dataProvider providesNewUsers
- */
- public function testUpdateOfUnchangedUser($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) {
- $user = $this->getUserMock($displayName, $eMailAddress, $cloudId);
-
- $converter = new Converter();
- $vCard = $converter->createCardFromUser($user);
- $updated = $converter->updateCard($vCard, $user);
- $this->assertFalse($updated);
- $cardData = $vCard->serialize();
-
- $this->assertEquals($expectedVCard, $cardData);
- }
-
- /**
- * @dataProvider providesUsersForUpdateOfRemovedElement
- */
- public function testUpdateOfRemovedElement($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) {
- $user = $this->getUserMock($displayName, $eMailAddress, $cloudId);
-
- $converter = new Converter();
- $vCard = $converter->createCardFromUser($user);
-
- $user1 = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock();
- $user1->method('getUID')->willReturn('12345');
- $user1->method('getDisplayName')->willReturn(null);
- $user1->method('getEMailAddress')->willReturn(null);
- $user1->method('getCloudId')->willReturn(null);
- $user1->method('getAvatarImage')->willReturn(null);
-
- $updated = $converter->updateCard($vCard, $user1);
- $this->assertTrue($updated);
- $cardData = $vCard->serialize();
-
- $this->assertEquals($expectedVCard, $cardData);
- }
-
- public function providesUsersForUpdateOfRemovedElement() {
- return [
- ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar"],
- ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar", "foo@bar.net"],
- ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar", null, "foo@bar.net"],
- ];
- }
-
- /**
- * @dataProvider providesNames
- * @param $expected
- * @param $fullName
- */
- public function testNameSplitter($expected, $fullName) {
-
- $converter = new Converter();
- $r = $converter->splitFullName($fullName);
- $r = implode(';', $r);
- $this->assertEquals($expected, $r);
- }
-
- public function providesNames() {
- return [
- ['Sauron;;;;', 'Sauron'],
- ['Baggins;Bilbo;;;', 'Bilbo Baggins'],
- ['Tolkien;John;Ronald Reuel;;', 'John Ronald Reuel Tolkien'],
- ];
- }
-
- /**
- * @param $displayName
- * @param $eMailAddress
- * @param $cloudId
- * @return \PHPUnit_Framework_MockObject_MockObject
- */
- protected function getUserMock($displayName, $eMailAddress, $cloudId) {
- $image0 = $this->getMockBuilder('OCP\IImage')->disableOriginalConstructor()->getMock();
- $image0->method('mimeType')->willReturn('JPEG');
- $image0->method('data')->willReturn('123456789');
- $user = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock();
- $user->method('getUID')->willReturn('12345');
- $user->method('getDisplayName')->willReturn($displayName);
- $user->method('getEMailAddress')->willReturn($eMailAddress);
- $user->method('getCloudId')->willReturn($cloudId);
- $user->method('getAvatarImage')->willReturn($image0);
- return $user;
- }
-}
diff --git a/apps/dav/tests/unit/carddav/sharing/plugintest.php b/apps/dav/tests/unit/carddav/sharing/plugintest.php
deleted file mode 100644
index f7159c2d22d..00000000000
--- a/apps/dav/tests/unit/carddav/sharing/plugintest.php
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\CardDAV;
-
-
-use OCA\DAV\DAV\Sharing\IShareable;
-use OCA\DAV\DAV\Sharing\Plugin;
-use OCA\DAV\Connector\Sabre\Auth;
-use OCP\IRequest;
-use Sabre\DAV\Server;
-use Sabre\DAV\SimpleCollection;
-use Sabre\HTTP\Request;
-use Sabre\HTTP\Response;
-use Test\TestCase;
-
-class PluginTest extends TestCase {
-
- /** @var Plugin */
- private $plugin;
- /** @var Server */
- private $server;
- /** @var IShareable | \PHPUnit_Framework_MockObject_MockObject */
- private $book;
-
- public function setUp() {
- parent::setUp();
-
- /** @var Auth | \PHPUnit_Framework_MockObject_MockObject $authBackend */
- $authBackend = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Auth')->disableOriginalConstructor()->getMock();
- $authBackend->method('isDavAuthenticated')->willReturn(true);
-
- /** @var IRequest $request */
- $request = $this->getMockBuilder('OCP\IRequest')->disableOriginalConstructor()->getMock();
- $this->plugin = new Plugin($authBackend, $request);
-
- $root = new SimpleCollection('root');
- $this->server = new \Sabre\DAV\Server($root);
- /** @var SimpleCollection $node */
- $this->book = $this->getMockBuilder('OCA\DAV\DAV\Sharing\IShareable')->disableOriginalConstructor()->getMock();
- $this->book->method('getName')->willReturn('addressbook1.vcf');
- $root->addChild($this->book);
- $this->plugin->initialize($this->server);
- }
-
- public function testSharing() {
-
- $this->book->expects($this->once())->method('updateShares')->with([[
- 'href' => 'principal:principals/admin',
- 'commonName' => null,
- 'summary' => null,
- 'readOnly' => false
- ]], ['mailto:wilfredo@example.com']);
-
- // setup request
- $request = new Request();
- $request->addHeader('Content-Type', 'application/xml');
- $request->setUrl('addressbook1.vcf');
- $request->setBody('<?xml version="1.0" encoding="utf-8" ?><CS:share xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns"><CS:set><D:href>principal:principals/admin</D:href><CS:read-write/></CS:set> <CS:remove><D:href>mailto:wilfredo@example.com</D:href></CS:remove></CS:share>');
- $response = new Response();
- $this->plugin->httpPost($request, $response);
- }
-}
diff --git a/apps/dav/tests/unit/carddav/syncservicetest.php b/apps/dav/tests/unit/carddav/syncservicetest.php
deleted file mode 100644
index e3ffaf472ed..00000000000
--- a/apps/dav/tests/unit/carddav/syncservicetest.php
+++ /dev/null
@@ -1,142 +0,0 @@
-<?php
-/**
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\CardDAV;
-
-use OCP\IUser;
-use OCP\IUserManager;
-use Test\TestCase;
-
-class SyncServiceTest extends TestCase {
- public function testEmptySync() {
- $backend = $this->getBackendMock(0, 0, 0);
-
- $ss = $this->getSyncServiceMock($backend, []);
- $return = $ss->syncRemoteAddressBook('', 'system', '1234567890', null, '1', 'principals/system/system', []);
- $this->assertEquals('sync-token-1', $return);
- }
-
- public function testSyncWithNewElement() {
- $backend = $this->getBackendMock(1, 0, 0);
- $backend->method('getCard')->willReturn(false);
-
- $ss = $this->getSyncServiceMock($backend, ['0' => [200 => '']]);
- $return = $ss->syncRemoteAddressBook('', 'system', '1234567890', null, '1', 'principals/system/system', []);
- $this->assertEquals('sync-token-1', $return);
- }
-
- public function testSyncWithUpdatedElement() {
- $backend = $this->getBackendMock(0, 1, 0);
- $backend->method('getCard')->willReturn(true);
-
- $ss = $this->getSyncServiceMock($backend, ['0' => [200 => '']]);
- $return = $ss->syncRemoteAddressBook('', 'system', '1234567890', null, '1', 'principals/system/system', []);
- $this->assertEquals('sync-token-1', $return);
- }
-
- public function testSyncWithDeletedElement() {
- $backend = $this->getBackendMock(0, 0, 1);
-
- $ss = $this->getSyncServiceMock($backend, ['0' => [404 => '']]);
- $return = $ss->syncRemoteAddressBook('', 'system', '1234567890', null, '1', 'principals/system/system', []);
- $this->assertEquals('sync-token-1', $return);
- }
-
- public function testEnsureSystemAddressBookExists() {
- /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDAVBackend')->disableOriginalConstructor()->getMock();
- $backend->expects($this->exactly(1))->method('createAddressBook');
- $backend->expects($this->at(0))->method('getAddressBooksByUri')->willReturn(null);
- $backend->expects($this->at(1))->method('getAddressBooksByUri')->willReturn([]);
-
- /** @var IUserManager $userManager */
- $userManager = $this->getMockBuilder('OCP\IUserManager')->disableOriginalConstructor()->getMock();
- $logger = $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock();
- $ss = new SyncService($backend, $userManager, $logger);
- $book = $ss->ensureSystemAddressBookExists('principals/users/adam', 'contacts', []);
- }
-
- public function testUpdateAndDeleteUser() {
- /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backend */
- $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDAVBackend')->disableOriginalConstructor()->getMock();
- $logger = $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock();
-
- $backend->expects($this->once())->method('createCard');
- $backend->expects($this->once())->method('updateCard');
- $backend->expects($this->once())->method('deleteCard');
-
- $backend->method('getCard')->willReturnOnConsecutiveCalls(false, [
- 'carddata' => "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.8//EN\r\nUID:test-user\r\nFN:test-user\r\nN:test-user;;;;\r\nEND:VCARD\r\n\r\n"
- ]);
-
- /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $userManager */
- $userManager = $this->getMockBuilder('OCP\IUserManager')->disableOriginalConstructor()->getMock();
-
- /** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */
- $user = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock();
- $user->method('getBackendClassName')->willReturn('unittest');
- $user->method('getUID')->willReturn('test-user');
-
- $ss = new SyncService($backend, $userManager, $logger);
- $ss->updateUser($user);
-
- $user->method('getDisplayName')->willReturn('A test user for unit testing');
-
- $ss->updateUser($user);
-
- $ss->deleteUser($user);
- }
-
- /**
- * @param int $createCount
- * @param int $updateCount
- * @param int $deleteCount
- * @return \PHPUnit_Framework_MockObject_MockObject
- */
- private function getBackendMock($createCount, $updateCount, $deleteCount) {
- $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDAVBackend')->disableOriginalConstructor()->getMock();
- $backend->expects($this->exactly($createCount))->method('createCard');
- $backend->expects($this->exactly($updateCount))->method('updateCard');
- $backend->expects($this->exactly($deleteCount))->method('deleteCard');
- return $backend;
- }
-
- /**
- * @param $backend
- * @param $response
- * @return SyncService|\PHPUnit_Framework_MockObject_MockObject
- */
- private function getSyncServiceMock($backend, $response) {
- $userManager = $this->getMockBuilder('OCP\IUserManager')->disableOriginalConstructor()->getMock();
- $logger = $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock();
- /** @var SyncService | \PHPUnit_Framework_MockObject_MockObject $ss */
- $ss = $this->getMock('OCA\DAV\CardDAV\SyncService', ['ensureSystemAddressBookExists', 'requestSyncReport', 'download'], [$backend, $userManager, $logger]);
- $ss->method('requestSyncReport')->withAnyParameters()->willReturn(['response' => $response, 'token' => 'sync-token-1']);
- $ss->method('ensureSystemAddressBookExists')->willReturn(['id' => 1]);
- $ss->method('download')->willReturn([
- 'body' => '',
- 'statusCode' => 200,
- 'headers' => []
- ]);
- return $ss;
- }
-
-}
diff --git a/apps/dav/tests/unit/comments/entitycollection.php b/apps/dav/tests/unit/comments/entitycollection.php
deleted file mode 100644
index 5bf155f12ba..00000000000
--- a/apps/dav/tests/unit/comments/entitycollection.php
+++ /dev/null
@@ -1,116 +0,0 @@
-<?php
-/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Comments;
-
-class EntityCollection extends \Test\TestCase {
-
- protected $commentsManager;
- protected $folder;
- protected $userManager;
- protected $logger;
- protected $collection;
- protected $userSession;
-
- public function setUp() {
- parent::setUp();
-
- $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager');
- $this->folder = $this->getMock('\OCP\Files\Folder');
- $this->userManager = $this->getMock('\OCP\IUserManager');
- $this->userSession = $this->getMock('\OCP\IUserSession');
- $this->logger = $this->getMock('\OCP\ILogger');
-
- $this->collection = new \OCA\DAV\Comments\EntityCollection(
- '19',
- 'files',
- $this->commentsManager,
- $this->folder,
- $this->userManager,
- $this->userSession,
- $this->logger
- );
- }
-
- public function testGetId() {
- $this->assertSame($this->collection->getId(), '19');
- }
-
- public function testGetChild() {
- $this->commentsManager->expects($this->once())
- ->method('get')
- ->with('55')
- ->will($this->returnValue($this->getMock('\OCP\Comments\IComment')));
-
- $node = $this->collection->getChild('55');
- $this->assertTrue($node instanceof \OCA\DAV\Comments\CommentNode);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotFound
- */
- public function testGetChildException() {
- $this->commentsManager->expects($this->once())
- ->method('get')
- ->with('55')
- ->will($this->throwException(new \OCP\Comments\NotFoundException()));
-
- $this->collection->getChild('55');
- }
-
- public function testGetChildren() {
- $this->commentsManager->expects($this->once())
- ->method('getForObject')
- ->with('files', '19')
- ->will($this->returnValue([$this->getMock('\OCP\Comments\IComment')]));
-
- $result = $this->collection->getChildren();
-
- $this->assertSame(count($result), 1);
- $this->assertTrue($result[0] instanceof \OCA\DAV\Comments\CommentNode);
- }
-
- public function testFindChildren() {
- $dt = new \DateTime('2016-01-10 18:48:00');
- $this->commentsManager->expects($this->once())
- ->method('getForObject')
- ->with('files', '19', 5, 15, $dt)
- ->will($this->returnValue([$this->getMock('\OCP\Comments\IComment')]));
-
- $result = $this->collection->findChildren(5, 15, $dt);
-
- $this->assertSame(count($result), 1);
- $this->assertTrue($result[0] instanceof \OCA\DAV\Comments\CommentNode);
- }
-
- public function testChildExistsTrue() {
- $this->assertTrue($this->collection->childExists('44'));
- }
-
- public function testChildExistsFalse() {
- $this->commentsManager->expects($this->once())
- ->method('get')
- ->with('44')
- ->will($this->throwException(new \OCP\Comments\NotFoundException()));
-
- $this->assertFalse($this->collection->childExists('44'));
- }
-}
diff --git a/apps/dav/tests/unit/comments/entitytypecollection.php b/apps/dav/tests/unit/comments/entitytypecollection.php
deleted file mode 100644
index f3aa2dbd71f..00000000000
--- a/apps/dav/tests/unit/comments/entitytypecollection.php
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php
-/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Comments;
-
-use OCA\DAV\Comments\EntityCollection as EntityCollectionImplemantation;
-
-class EntityTypeCollection extends \Test\TestCase {
-
- protected $commentsManager;
- protected $folder;
- protected $userManager;
- protected $logger;
- protected $collection;
- protected $userSession;
-
- public function setUp() {
- parent::setUp();
-
- $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager');
- $this->folder = $this->getMock('\OCP\Files\Folder');
- $this->userManager = $this->getMock('\OCP\IUserManager');
- $this->userSession = $this->getMock('\OCP\IUserSession');
- $this->logger = $this->getMock('\OCP\ILogger');
-
- $this->collection = new \OCA\DAV\Comments\EntityTypeCollection(
- 'files',
- $this->commentsManager,
- $this->folder,
- $this->userManager,
- $this->userSession,
- $this->logger
- );
- }
-
- public function testChildExistsYes() {
- $this->folder->expects($this->once())
- ->method('getById')
- ->with('17')
- ->will($this->returnValue([$this->getMock('\OCP\Files\Node')]));
- $this->assertTrue($this->collection->childExists('17'));
- }
-
- public function testChildExistsNo() {
- $this->folder->expects($this->once())
- ->method('getById')
- ->will($this->returnValue([]));
- $this->assertFalse($this->collection->childExists('17'));
- }
-
- public function testGetChild() {
- $this->folder->expects($this->once())
- ->method('getById')
- ->with('17')
- ->will($this->returnValue([$this->getMock('\OCP\Files\Node')]));
-
- $ec = $this->collection->getChild('17');
- $this->assertTrue($ec instanceof EntityCollectionImplemantation);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotFound
- */
- public function testGetChildException() {
- $this->folder->expects($this->once())
- ->method('getById')
- ->with('17')
- ->will($this->returnValue([]));
-
- $this->collection->getChild('17');
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\MethodNotAllowed
- */
- public function testGetChildren() {
- $this->collection->getChildren();
- }
-}
diff --git a/apps/dav/tests/unit/comments/rootcollection.php b/apps/dav/tests/unit/comments/rootcollection.php
deleted file mode 100644
index 369006e7159..00000000000
--- a/apps/dav/tests/unit/comments/rootcollection.php
+++ /dev/null
@@ -1,160 +0,0 @@
-<?php
-/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Comments;
-
-use OCA\DAV\Comments\EntityTypeCollection as EntityTypeCollectionImplementation;
-
-class RootCollection extends \Test\TestCase {
-
- protected $commentsManager;
- protected $userManager;
- protected $logger;
- protected $collection;
- protected $userSession;
- protected $rootFolder;
- protected $user;
-
- public function setUp() {
- parent::setUp();
-
- $this->user = $this->getMock('\OCP\IUser');
-
- $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager');
- $this->userManager = $this->getMock('\OCP\IUserManager');
- $this->userSession = $this->getMock('\OCP\IUserSession');
- $this->rootFolder = $this->getMock('\OCP\Files\IRootFolder');
- $this->logger = $this->getMock('\OCP\ILogger');
-
- $this->collection = new \OCA\DAV\Comments\RootCollection(
- $this->commentsManager,
- $this->userManager,
- $this->userSession,
- $this->rootFolder,
- $this->logger
- );
- }
-
- protected function prepareForInitCollections() {
- $this->user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('alice'));
-
- $this->userSession->expects($this->once())
- ->method('getUser')
- ->will($this->returnValue($this->user));
-
- $this->rootFolder->expects($this->once())
- ->method('getUserFolder')
- ->with('alice')
- ->will($this->returnValue($this->getMock('\OCP\Files\Folder')));
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testCreateFile() {
- $this->collection->createFile('foo');
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testCreateDirectory() {
- $this->collection->createDirectory('foo');
- }
-
- public function testGetChild() {
- $this->prepareForInitCollections();
- $etc = $this->collection->getChild('files');
- $this->assertTrue($etc instanceof EntityTypeCollectionImplementation);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotFound
- */
- public function testGetChildInvalid() {
- $this->prepareForInitCollections();
- $this->collection->getChild('robots');
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotAuthenticated
- */
- public function testGetChildNoAuth() {
- $this->collection->getChild('files');
- }
-
- public function testGetChildren() {
- $this->prepareForInitCollections();
- $children = $this->collection->getChildren();
- $this->assertFalse(empty($children));
- foreach($children as $child) {
- $this->assertTrue($child instanceof EntityTypeCollectionImplementation);
- }
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotAuthenticated
- */
- public function testGetChildrenNoAuth() {
- $this->collection->getChildren();
- }
-
- public function testChildExistsYes() {
- $this->prepareForInitCollections();
- $this->assertTrue($this->collection->childExists('files'));
- }
-
- public function testChildExistsNo() {
- $this->prepareForInitCollections();
- $this->assertFalse($this->collection->childExists('robots'));
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotAuthenticated
- */
- public function testChildExistsNoAuth() {
- $this->collection->childExists('files');
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testDelete() {
- $this->collection->delete();
- }
-
- public function testGetName() {
- $this->assertSame('comments', $this->collection->getName());
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testSetName() {
- $this->collection->setName('foobar');
- }
-
- public function testGetLastModified() {
- $this->assertSame(null, $this->collection->getLastModified());
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php b/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php
deleted file mode 100644
index d02064531ab..00000000000
--- a/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php
+++ /dev/null
@@ -1,130 +0,0 @@
-<?php
-/**
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
-use Test\TestCase;
-use OCP\IConfig;
-
-/**
- * Class BlockLegacyClientPluginTest
- *
- * @package Test\Connector\Sabre
- */
-class BlockLegacyClientPluginTest extends TestCase {
- /** @var IConfig */
- private $config;
- /** @var BlockLegacyClientPlugin */
- private $blockLegacyClientVersionPlugin;
-
- public function setUp() {
- parent::setUp();
-
- $this->config = $this->getMock('\OCP\IConfig');
- $this->blockLegacyClientVersionPlugin = new BlockLegacyClientPlugin($this->config);
- }
-
- /**
- * @return array
- */
- public function oldDesktopClientProvider() {
- return [
- ['Mozilla/5.0 (1.5.0) mirall/1.5.0'],
- ['mirall/1.5.0'],
- ['mirall/1.5.4'],
- ['mirall/1.6.0'],
- ['Mozilla/5.0 (Bogus Text) mirall/1.6.9'],
- ];
- }
-
- /**
- * @dataProvider oldDesktopClientProvider
- * @param string $userAgent
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Unsupported client version.
- */
- public function testBeforeHandlerException($userAgent) {
- /** @var \Sabre\HTTP\RequestInterface $request */
- $request = $this->getMock('\Sabre\HTTP\RequestInterface');
- $request
- ->expects($this->once())
- ->method('getHeader')
- ->with('User-Agent')
- ->will($this->returnValue($userAgent));
-
- $this->config
- ->expects($this->once())
- ->method('getSystemValue')
- ->with('minimum.supported.desktop.version', '1.7.0')
- ->will($this->returnValue('1.7.0'));
-
- $this->blockLegacyClientVersionPlugin->beforeHandler($request);
- }
-
- /**
- * @return array
- */
- public function newAndAlternateDesktopClientProvider() {
- return [
- ['Mozilla/5.0 (1.7.0) mirall/1.7.0'],
- ['mirall/1.8.3'],
- ['mirall/1.7.2'],
- ['mirall/1.7.0'],
- ['Mozilla/5.0 (Bogus Text) mirall/1.9.3'],
- ];
- }
-
- /**
- * @dataProvider newAndAlternateDesktopClientProvider
- * @param string $userAgent
- */
- public function testBeforeHandlerSuccess($userAgent) {
- /** @var \Sabre\HTTP\RequestInterface $request */
- $request = $this->getMock('\Sabre\HTTP\RequestInterface');
- $request
- ->expects($this->once())
- ->method('getHeader')
- ->with('User-Agent')
- ->will($this->returnValue($userAgent));
-
- $this->config
- ->expects($this->once())
- ->method('getSystemValue')
- ->with('minimum.supported.desktop.version', '1.7.0')
- ->will($this->returnValue('1.7.0'));
-
- $this->blockLegacyClientVersionPlugin->beforeHandler($request);
- }
-
- public function testBeforeHandlerNoUserAgent() {
- /** @var \Sabre\HTTP\RequestInterface $request */
- $request = $this->getMock('\Sabre\HTTP\RequestInterface');
- $request
- ->expects($this->once())
- ->method('getHeader')
- ->with('User-Agent')
- ->will($this->returnValue(null));
- $this->blockLegacyClientVersionPlugin->beforeHandler($request);
- }
-
-}
diff --git a/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php b/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php
deleted file mode 100644
index 0ead617f461..00000000000
--- a/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-/**
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin;
-use Test\TestCase;
-
-/**
- * Class DummyGetResponsePluginTest
- *
- * @package Test\Connector\Sabre
- */
-class DummyGetResponsePluginTest extends TestCase {
- /** @var DummyGetResponsePlugin */
- private $dummyGetResponsePlugin;
-
- public function setUp() {
- parent::setUp();
-
- $this->dummyGetResponsePlugin = new DummyGetResponsePlugin();
- }
-
- public function testInitialize() {
- /** @var \Sabre\DAV\Server $server */
- $server = $this->getMock('\Sabre\DAV\Server');
- $server
- ->expects($this->once())
- ->method('on')
- ->with('method:GET', [$this->dummyGetResponsePlugin, 'httpGet'], 200);
-
- $this->dummyGetResponsePlugin->initialize($server);
- }
-
-
- public function testHttpGet() {
- /** @var \Sabre\HTTP\RequestInterface $request */
- $request = $this->getMock('\Sabre\HTTP\RequestInterface');
- /** @var \Sabre\HTTP\ResponseInterface $response */
- $response = $server = $this->getMock('\Sabre\HTTP\ResponseInterface');
- $response
- ->expects($this->once())
- ->method('setBody');
- $response
- ->expects($this->once())
- ->method('setStatus')
- ->with(200);
-
- $this->assertSame(false, $this->dummyGetResponsePlugin->httpGet($request, $response));
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php b/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php
deleted file mode 100644
index 30d2bf41810..00000000000
--- a/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php
+++ /dev/null
@@ -1,174 +0,0 @@
-<?php
-/**
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OCA\DAV\Connector\Sabre\FakeLockerPlugin;
-use Sabre\HTTP\Response;
-use Test\TestCase;
-
-/**
- * Class FakeLockerPluginTest
- *
- * @package OCA\DAV\Tests\Unit\Connector\Sabre
- */
-class FakeLockerPluginTest extends TestCase {
- /** @var FakeLockerPlugin */
- private $fakeLockerPlugin;
-
- public function setUp() {
- parent::setUp();
- $this->fakeLockerPlugin = new FakeLockerPlugin();
- }
-
- public function testInitialize() {
- /** @var \Sabre\DAV\Server $server */
- $server = $this->getMock('\Sabre\DAV\Server');
- $server
- ->expects($this->at(0))
- ->method('on')
- ->with('method:LOCK', [$this->fakeLockerPlugin, 'fakeLockProvider'], 1);
- $server
- ->expects($this->at(1))
- ->method('on')
- ->with('method:UNLOCK', [$this->fakeLockerPlugin, 'fakeUnlockProvider'], 1);
- $server
- ->expects($this->at(2))
- ->method('on')
- ->with('propFind', [$this->fakeLockerPlugin, 'propFind']);
- $server
- ->expects($this->at(3))
- ->method('on')
- ->with('validateTokens', [$this->fakeLockerPlugin, 'validateTokens']);
-
- $this->fakeLockerPlugin->initialize($server);
- }
-
- public function testGetHTTPMethods() {
- $expected = [
- 'LOCK',
- 'UNLOCK',
- ];
- $this->assertSame($expected, $this->fakeLockerPlugin->getHTTPMethods('Test'));
- }
-
- public function testGetFeatures() {
- $expected = [
- 2,
- ];
- $this->assertSame($expected, $this->fakeLockerPlugin->getFeatures());
- }
-
- public function testPropFind() {
- $propFind = $this->getMockBuilder('\Sabre\DAV\PropFind')
- ->disableOriginalConstructor()
- ->getMock();
- $node = $this->getMock('\Sabre\DAV\INode');
-
- $propFind->expects($this->at(0))
- ->method('handle')
- ->with('{DAV:}supportedlock');
- $propFind->expects($this->at(1))
- ->method('handle')
- ->with('{DAV:}lockdiscovery');
-
- $this->fakeLockerPlugin->propFind($propFind, $node);
- }
-
- public function tokenDataProvider() {
- return [
- [
- [
- [
- 'tokens' => [
- [
- 'token' => 'aToken',
- 'validToken' => false,
- ],
- [],
- [
- 'token' => 'opaquelocktoken:asdf',
- 'validToken' => false,
- ]
- ],
- ]
- ],
- [
- [
- 'tokens' => [
- [
- 'token' => 'aToken',
- 'validToken' => false,
- ],
- [],
- [
- 'token' => 'opaquelocktoken:asdf',
- 'validToken' => true,
- ]
- ],
- ]
- ],
- ]
- ];
- }
-
- /**
- * @dataProvider tokenDataProvider
- * @param array $input
- * @param array $expected
- */
- public function testValidateTokens(array $input, array $expected) {
- $request = $this->getMock('\Sabre\HTTP\RequestInterface');
- $this->fakeLockerPlugin->validateTokens($request, $input);
- $this->assertSame($expected, $input);
- }
-
- public function testFakeLockProvider() {
- $request = $this->getMock('\Sabre\HTTP\RequestInterface');
- $response = new Response();
- $server = $this->getMock('\Sabre\DAV\Server');
- $this->fakeLockerPlugin->initialize($server);
-
- $request->expects($this->exactly(2))
- ->method('getPath')
- ->will($this->returnValue('MyPath'));
-
- $this->assertSame(false, $this->fakeLockerPlugin->fakeLockProvider($request, $response));
-
- $expectedXml = '<?xml version="1.0" encoding="utf-8"?><d:prop xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"><d:lockdiscovery><d:activelock><d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype><d:lockroot><d:href>MyPath</d:href></d:lockroot><d:depth>infinity</d:depth><d:timeout>Second-1800</d:timeout><d:locktoken><d:href>opaquelocktoken:fe4f7f2437b151fbcb4e9f5c8118c6b1</d:href></d:locktoken><d:owner/></d:activelock></d:lockdiscovery></d:prop>';
-
- $this->assertXmlStringEqualsXmlString($expectedXml, $response->getBody());
- }
-
- public function testFakeUnlockProvider() {
- $request = $this->getMock('\Sabre\HTTP\RequestInterface');
- $response = $this->getMock('\Sabre\HTTP\ResponseInterface');
-
- $response->expects($this->once())
- ->method('setStatus')
- ->with('204');
- $response->expects($this->once())
- ->method('setHeader')
- ->with('Content-Length', '0');
-
- $this->assertSame(false, $this->fakeLockerPlugin->fakeUnlockProvider($request, $response));
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php b/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php
deleted file mode 100644
index dea1e64db1d..00000000000
--- a/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OCA\DAV\Connector\Sabre\MaintenancePlugin;
-use Test\TestCase;
-use OCP\IConfig;
-
-/**
- * Class MaintenancePluginTest
- *
- * @package Test\Connector\Sabre
- */
-class MaintenancePluginTest extends TestCase {
- /** @var IConfig */
- private $config;
- /** @var MaintenancePlugin */
- private $maintenancePlugin;
-
- public function setUp() {
- parent::setUp();
-
- $this->config = $this->getMock('\OCP\IConfig');
- $this->maintenancePlugin = new MaintenancePlugin($this->config);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\ServiceUnavailable
- * @expectedExceptionMessage System in single user mode.
- */
- public function testSingleUserMode() {
- $this->config
- ->expects($this->once())
- ->method('getSystemValue')
- ->with('singleuser', false)
- ->will($this->returnValue(true));
-
- $this->maintenancePlugin->checkMaintenanceMode();
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\ServiceUnavailable
- * @expectedExceptionMessage System in single user mode.
- */
- public function testMaintenanceMode() {
- $this->config
- ->expects($this->exactly(1))
- ->method('getSystemValue')
- ->will($this->onConsecutiveCalls([false, true]));
-
- $this->maintenancePlugin->checkMaintenanceMode();
- }
-
-}
diff --git a/apps/dav/tests/unit/connector/sabre/auth.php b/apps/dav/tests/unit/connector/sabre/auth.php
deleted file mode 100644
index b81a5e003b5..00000000000
--- a/apps/dav/tests/unit/connector/sabre/auth.php
+++ /dev/null
@@ -1,604 +0,0 @@
-<?php
-/**
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Roeland Jago Douma <rullzer@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OCP\IRequest;
-use OCP\IUser;
-use Test\TestCase;
-use OCP\ISession;
-use OCP\IUserSession;
-
-/**
- * Class Auth
- *
- * @package OCA\DAV\Connector\Sabre
- * @group DB
- */
-class Auth extends TestCase {
- /** @var ISession */
- private $session;
- /** @var \OCA\DAV\Connector\Sabre\Auth */
- private $auth;
- /** @var IUserSession */
- private $userSession;
- /** @var IRequest */
- private $request;
-
- public function setUp() {
- parent::setUp();
- $this->session = $this->getMockBuilder('\OCP\ISession')
- ->disableOriginalConstructor()->getMock();
- $this->userSession = $this->getMockBuilder('\OCP\IUserSession')
- ->disableOriginalConstructor()->getMock();
- $this->request = $this->getMockBuilder('\OCP\IRequest')
- ->disableOriginalConstructor()->getMock();
- $this->auth = new \OCA\DAV\Connector\Sabre\Auth(
- $this->session,
- $this->userSession,
- $this->request
- );
- }
-
- public function testIsDavAuthenticatedWithoutDavSession() {
- $this->session
- ->expects($this->once())
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue(null));
-
- $this->assertFalse($this->invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
- }
-
- public function testIsDavAuthenticatedWithWrongDavSession() {
- $this->session
- ->expects($this->exactly(2))
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue('AnotherUser'));
-
- $this->assertFalse($this->invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
- }
-
- public function testIsDavAuthenticatedWithCorrectDavSession() {
- $this->session
- ->expects($this->exactly(2))
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue('MyTestUser'));
-
- $this->assertTrue($this->invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
- }
-
- public function testValidateUserPassOfAlreadyDAVAuthenticatedUser() {
- $user = $this->getMockBuilder('\OCP\IUser')
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->exactly(2))
- ->method('getUID')
- ->will($this->returnValue('MyTestUser'));
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->will($this->returnValue(true));
- $this->userSession
- ->expects($this->exactly(2))
- ->method('getUser')
- ->will($this->returnValue($user));
- $this->session
- ->expects($this->exactly(2))
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue('MyTestUser'));
- $this->session
- ->expects($this->once())
- ->method('close');
-
- $this->assertTrue($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
- }
-
- public function testValidateUserPassOfInvalidDAVAuthenticatedUser() {
- $user = $this->getMockBuilder('\OCP\IUser')
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->once())
- ->method('getUID')
- ->will($this->returnValue('MyTestUser'));
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->will($this->returnValue(true));
- $this->userSession
- ->expects($this->once())
- ->method('getUser')
- ->will($this->returnValue($user));
- $this->session
- ->expects($this->exactly(2))
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue('AnotherUser'));
- $this->session
- ->expects($this->once())
- ->method('close');
-
- $this->assertFalse($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
- }
-
- public function testValidateUserPassOfInvalidDAVAuthenticatedUserWithValidPassword() {
- $user = $this->getMockBuilder('\OCP\IUser')
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->exactly(3))
- ->method('getUID')
- ->will($this->returnValue('MyTestUser'));
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->will($this->returnValue(true));
- $this->userSession
- ->expects($this->exactly(3))
- ->method('getUser')
- ->will($this->returnValue($user));
- $this->session
- ->expects($this->exactly(2))
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue('AnotherUser'));
- $this->userSession
- ->expects($this->once())
- ->method('login')
- ->with('MyTestUser', 'MyTestPassword')
- ->will($this->returnValue(true));
- $this->session
- ->expects($this->once())
- ->method('set')
- ->with('AUTHENTICATED_TO_DAV_BACKEND', 'MyTestUser');
- $this->session
- ->expects($this->once())
- ->method('close');
-
- $this->assertTrue($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
- }
-
- public function testValidateUserPassWithInvalidPassword() {
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->will($this->returnValue(false));
- $this->userSession
- ->expects($this->once())
- ->method('login')
- ->with('MyTestUser', 'MyTestPassword')
- ->will($this->returnValue(false));
- $this->session
- ->expects($this->once())
- ->method('close');
-
- $this->assertFalse($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
- }
-
-
- public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGet() {
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession
- ->expects($this->any())
- ->method('isLoggedIn')
- ->will($this->returnValue(true));
- $this->request
- ->expects($this->any())
- ->method('getMethod')
- ->willReturn('POST');
- $this->session
- ->expects($this->any())
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue(null));
- $user = $this->getMockBuilder('\OCP\IUser')
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('MyWrongDavUser'));
- $this->userSession
- ->expects($this->any())
- ->method('getUser')
- ->will($this->returnValue($user));
- $this->request
- ->expects($this->once())
- ->method('passesCSRFCheck')
- ->willReturn(false);
-
- $expectedResponse = [
- false,
- "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is mis-configured",
- ];
- $response = $this->auth->check($request, $response);
- $this->assertSame($expectedResponse, $response);
- }
-
- public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndCorrectlyDavAuthenticated() {
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession
- ->expects($this->any())
- ->method('isLoggedIn')
- ->willReturn(true);
- $this->request
- ->expects($this->any())
- ->method('getMethod')
- ->willReturn('PROPFIND');
- $this->request
- ->expects($this->any())
- ->method('isUserAgent')
- ->with([
- '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/',
- '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/',
- '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/',
- ])
- ->willReturn(false);
- $this->session
- ->expects($this->any())
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue('LoggedInUser'));
- $user = $this->getMockBuilder('\OCP\IUser')
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('LoggedInUser'));
- $this->userSession
- ->expects($this->any())
- ->method('getUser')
- ->will($this->returnValue($user));
- $this->request
- ->expects($this->once())
- ->method('passesCSRFCheck')
- ->willReturn(false);
- $this->auth->check($request, $response);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotAuthenticated
- * @expectedExceptionMessage CSRF check not passed.
- */
- public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndIncorrectlyDavAuthenticated() {
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession
- ->expects($this->any())
- ->method('isLoggedIn')
- ->willReturn(true);
- $this->request
- ->expects($this->any())
- ->method('getMethod')
- ->willReturn('PROPFIND');
- $this->request
- ->expects($this->any())
- ->method('isUserAgent')
- ->with([
- '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/',
- '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/',
- '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/',
- ])
- ->willReturn(false);
- $this->session
- ->expects($this->any())
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue('AnotherUser'));
- $user = $this->getMockBuilder('\OCP\IUser')
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('LoggedInUser'));
- $this->userSession
- ->expects($this->any())
- ->method('getUser')
- ->will($this->returnValue($user));
- $this->request
- ->expects($this->once())
- ->method('passesCSRFCheck')
- ->willReturn(false);
- $this->auth->check($request, $response);
- }
-
- public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGetAndDesktopClient() {
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession
- ->expects($this->any())
- ->method('isLoggedIn')
- ->will($this->returnValue(true));
- $this->request
- ->expects($this->any())
- ->method('getMethod')
- ->willReturn('POST');
- $this->request
- ->expects($this->any())
- ->method('isUserAgent')
- ->with([
- '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/',
- '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/',
- '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/',
- ])
- ->willReturn(true);
- $this->session
- ->expects($this->any())
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue(null));
- $user = $this->getMockBuilder('\OCP\IUser')
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('MyWrongDavUser'));
- $this->userSession
- ->expects($this->any())
- ->method('getUser')
- ->will($this->returnValue($user));
- $this->request
- ->expects($this->once())
- ->method('passesCSRFCheck')
- ->willReturn(false);
-
- $this->auth->check($request, $response);
- }
-
- public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForGet() {
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession
- ->expects($this->any())
- ->method('isLoggedIn')
- ->will($this->returnValue(true));
- $this->session
- ->expects($this->any())
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue(null));
- $user = $this->getMockBuilder('\OCP\IUser')
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('MyWrongDavUser'));
- $this->userSession
- ->expects($this->any())
- ->method('getUser')
- ->will($this->returnValue($user));
- $this->request
- ->expects($this->any())
- ->method('getMethod')
- ->willReturn('GET');
-
- $response = $this->auth->check($request, $response);
- $this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response);
- }
-
- public function testAuthenticateAlreadyLoggedInWithCsrfTokenForGet() {
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession
- ->expects($this->any())
- ->method('isLoggedIn')
- ->will($this->returnValue(true));
- $this->session
- ->expects($this->any())
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue(null));
- $user = $this->getMockBuilder('\OCP\IUser')
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('MyWrongDavUser'));
- $this->userSession
- ->expects($this->any())
- ->method('getUser')
- ->will($this->returnValue($user));
- $this->request
- ->expects($this->once())
- ->method('passesCSRFCheck')
- ->willReturn(true);
-
- $response = $this->auth->check($request, $response);
- $this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response);
- }
-
- public function testAuthenticateNoBasicAuthenticateHeadersProvided() {
- $server = $this->getMockBuilder('\Sabre\DAV\Server')
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->auth->check($server->httpRequest, $server->httpResponse);
- $this->assertEquals([false, 'No \'Authorization: Basic\' header found. Either the client didn\'t send one, or the server is mis-configured'], $response);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotAuthenticated
- * @expectedExceptionMessage Cannot authenticate over ajax calls
- */
- public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjax() {
- /** @var \Sabre\HTTP\RequestInterface $httpRequest */
- $httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- /** @var \Sabre\HTTP\ResponseInterface $httpResponse */
- $httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession
- ->expects($this->any())
- ->method('isLoggedIn')
- ->will($this->returnValue(false));
- $httpRequest
- ->expects($this->once())
- ->method('getHeader')
- ->with('X-Requested-With')
- ->will($this->returnValue('XMLHttpRequest'));
- $this->auth->check($httpRequest, $httpResponse);
- }
-
- public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjaxButUserIsStillLoggedIn() {
- /** @var \Sabre\HTTP\RequestInterface $httpRequest */
- $httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- /** @var \Sabre\HTTP\ResponseInterface $httpResponse */
- $httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- /** @var IUser */
- $user = $this->getMock('OCP\IUser');
- $user->method('getUID')->willReturn('MyTestUser');
- $this->userSession
- ->expects($this->any())
- ->method('isLoggedIn')
- ->will($this->returnValue(true));
- $this->userSession
- ->expects($this->any())
- ->method('getUser')
- ->willReturn($user);
- $this->session
- ->expects($this->atLeastOnce())
- ->method('get')
- ->with('AUTHENTICATED_TO_DAV_BACKEND')
- ->will($this->returnValue('MyTestUser'));
- $this->request
- ->expects($this->once())
- ->method('getMethod')
- ->willReturn('GET');
- $httpRequest
- ->expects($this->atLeastOnce())
- ->method('getHeader')
- ->with('Authorization')
- ->will($this->returnValue(null));
- $this->assertEquals(
- [true, 'principals/users/MyTestUser'],
- $this->auth->check($httpRequest, $httpResponse)
- );
- }
-
- public function testAuthenticateValidCredentials() {
- $server = $this->getMockBuilder('\Sabre\DAV\Server')
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpRequest
- ->expects($this->at(0))
- ->method('getHeader')
- ->with('X-Requested-With')
- ->will($this->returnValue(null));
- $server->httpRequest
- ->expects($this->at(1))
- ->method('getHeader')
- ->with('Authorization')
- ->will($this->returnValue('basic dXNlcm5hbWU6cGFzc3dvcmQ='));
- $server->httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession
- ->expects($this->once())
- ->method('login')
- ->with('username', 'password')
- ->will($this->returnValue(true));
- $user = $this->getMockBuilder('\OCP\IUser')
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->exactly(3))
- ->method('getUID')
- ->will($this->returnValue('MyTestUser'));
- $this->userSession
- ->expects($this->exactly(3))
- ->method('getUser')
- ->will($this->returnValue($user));
- $response = $this->auth->check($server->httpRequest, $server->httpResponse);
- $this->assertEquals([true, 'principals/users/MyTestUser'], $response);
- }
-
- public function testAuthenticateInvalidCredentials() {
- $server = $this->getMockBuilder('\Sabre\DAV\Server')
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpRequest
- ->expects($this->at(0))
- ->method('getHeader')
- ->with('X-Requested-With')
- ->will($this->returnValue(null));
- $server->httpRequest
- ->expects($this->at(1))
- ->method('getHeader')
- ->with('Authorization')
- ->will($this->returnValue('basic dXNlcm5hbWU6cGFzc3dvcmQ='));
- $server->httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession
- ->expects($this->once())
- ->method('login')
- ->with('username', 'password')
- ->will($this->returnValue(false));
- $response = $this->auth->check($server->httpRequest, $server->httpResponse);
- $this->assertEquals([false, 'Username or password was incorrect'], $response);
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/commentpropertiesplugin.php b/apps/dav/tests/unit/connector/sabre/commentpropertiesplugin.php
deleted file mode 100644
index f915c83c4a7..00000000000
--- a/apps/dav/tests/unit/connector/sabre/commentpropertiesplugin.php
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php
-/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use \OCA\DAV\Connector\Sabre\CommentPropertiesPlugin as CommentPropertiesPluginImplementation;
-
-class CommentsPropertiesPlugin extends \Test\TestCase {
-
- /** @var CommentPropertiesPluginImplementation */
- protected $plugin;
- protected $commentsManager;
- protected $userSession;
- protected $server;
-
- public function setUp() {
- parent::setUp();
-
- $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager');
- $this->userSession = $this->getMock('\OCP\IUserSession');
-
- $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->plugin = new CommentPropertiesPluginImplementation($this->commentsManager, $this->userSession);
- $this->plugin->initialize($this->server);
- }
-
- public function nodeProvider() {
- $mocks = [];
- foreach(['\OCA\DAV\Connector\Sabre\File', '\OCA\DAV\Connector\Sabre\Directory', '\Sabre\DAV\INode'] as $class) {
- $mocks[] = $this->getMockBuilder($class)
- ->disableOriginalConstructor()
- ->getMock();
- }
-
- return [
- [$mocks[0], true],
- [$mocks[1], true],
- [$mocks[2], false]
- ];
- }
-
- /**
- * @dataProvider nodeProvider
- * @param $node
- * @param $expectedSuccessful
- */
- public function testHandleGetProperties($node, $expectedSuccessful) {
- $propFind = $this->getMockBuilder('\Sabre\DAV\PropFind')
- ->disableOriginalConstructor()
- ->getMock();
-
- if($expectedSuccessful) {
- $propFind->expects($this->exactly(3))
- ->method('handle');
- } else {
- $propFind->expects($this->never())
- ->method('handle');
- }
-
- $this->plugin->handleGetProperties($propFind, $node);
- }
-
- public function baseUriProvider() {
- return [
- ['owncloud/remote.php/webdav/', '4567', 'owncloud/remote.php/dav/comments/files/4567'],
- ['owncloud/remote.php/wicked/', '4567', null]
- ];
- }
-
- /**
- * @dataProvider baseUriProvider
- * @param $baseUri
- * @param $fid
- * @param $expectedHref
- */
- public function testGetCommentsLink($baseUri, $fid, $expectedHref) {
- $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getId')
- ->will($this->returnValue($fid));
-
- $this->server->expects($this->once())
- ->method('getBaseUri')
- ->will($this->returnValue($baseUri));
-
- $href = $this->plugin->getCommentsLink($node);
- $this->assertSame($expectedHref, $href);
- }
-
- public function userProvider() {
- return [
- [$this->getMock('\OCP\IUser')],
- [null]
- ];
- }
-
- /**
- * @dataProvider userProvider
- * @param $user
- */
- public function testGetUnreadCount($user) {
- $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getId')
- ->will($this->returnValue('4567'));
-
- $this->userSession->expects($this->once())
- ->method('getUser')
- ->will($this->returnValue($user));
-
- $this->commentsManager->expects($this->any())
- ->method('getNumberOfCommentsForObject')
- ->will($this->returnValue(42));
-
- $unread = $this->plugin->getUnreadCount($node);
- if(is_null($user)) {
- $this->assertNull($unread);
- } else {
- $this->assertSame($unread, 42);
- }
- }
-
-}
diff --git a/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php b/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php
deleted file mode 100644
index 7f6fb26e4d1..00000000000
--- a/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-/**
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- * This file is licensed under the Affero General Public License version 3 or
- * later.
- * See the COPYING-README file.
- */
-class CopyEtagPluginTest extends \Test\TestCase {
-
- /**
- * @var \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin
- */
- private $plugin;
-
- public function setUp() {
- parent::setUp();
- $this->server = new \Sabre\DAV\Server();
- $this->plugin = new \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin();
- $this->plugin->initialize($this->server);
- }
-
- public function testCopyEtag() {
- $request = new \Sabre\Http\Request();
- $response = new \Sabre\Http\Response();
- $response->setHeader('Etag', 'abcd');
-
- $this->plugin->afterMethod($request, $response);
-
- $this->assertEquals('abcd', $response->getHeader('OC-Etag'));
- }
-
- public function testNoopWhenEmpty() {
- $request = new \Sabre\Http\Request();
- $response = new \Sabre\Http\Response();
-
- $this->plugin->afterMethod($request, $response);
-
- $this->assertNull($response->getHeader('OC-Etag'));
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php b/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php
deleted file mode 100644
index e0ba61e9134..00000000000
--- a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php
+++ /dev/null
@@ -1,313 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-/**
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- * This file is licensed under the Affero General Public License version 3 or
- * later.
- * See the COPYING-README file.
- */
-
-/**
- * Class CustomPropertiesBackend
- *
- * @group DB
- *
- * @package Tests\Connector\Sabre
- */
-class CustomPropertiesBackend extends \Test\TestCase {
-
- /**
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var \Sabre\DAV\Tree
- */
- private $tree;
-
- /**
- * @var \OCA\DAV\Connector\Sabre\CustomPropertiesBackend
- */
- private $plugin;
-
- /**
- * @var \OCP\IUser
- */
- private $user;
-
- public function setUp() {
- parent::setUp();
- $this->server = new \Sabre\DAV\Server();
- $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
- ->disableOriginalConstructor()
- ->getMock();
-
- $userId = $this->getUniqueID('testcustompropertiesuser');
-
- $this->user = $this->getMock('\OCP\IUser');
- $this->user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue($userId));
-
- $this->plugin = new \OCA\DAV\Connector\Sabre\CustomPropertiesBackend(
- $this->tree,
- \OC::$server->getDatabaseConnection(),
- $this->user
- );
- }
-
- public function tearDown() {
- $connection = \OC::$server->getDatabaseConnection();
- $deleteStatement = $connection->prepare(
- 'DELETE FROM `*PREFIX*properties`' .
- ' WHERE `userid` = ?'
- );
- $deleteStatement->execute(
- array(
- $this->user->getUID(),
- )
- );
- $deleteStatement->closeCursor();
- }
-
- private function createTestNode($class) {
- $node = $this->getMockBuilder($class)
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(123));
-
- $node->expects($this->any())
- ->method('getPath')
- ->will($this->returnValue('/dummypath'));
-
- return $node;
- }
-
- private function applyDefaultProps($path = '/dummypath') {
- // properties to set
- $propPatch = new \Sabre\DAV\PropPatch(array(
- 'customprop' => 'value1',
- 'customprop2' => 'value2',
- ));
-
- $this->plugin->propPatch(
- $path,
- $propPatch
- );
-
- $propPatch->commit();
-
- $this->assertEmpty($propPatch->getRemainingMutations());
-
- $result = $propPatch->getResult();
- $this->assertEquals(200, $result['customprop']);
- $this->assertEquals(200, $result['customprop2']);
- }
-
- /**
- * Test that propFind on a missing file soft fails
- */
- public function testPropFindMissingFileSoftFail() {
- $this->tree->expects($this->at(0))
- ->method('getNodeForPath')
- ->with('/dummypath')
- ->will($this->throwException(new \Sabre\DAV\Exception\NotFound()));
-
- $this->tree->expects($this->at(1))
- ->method('getNodeForPath')
- ->with('/dummypath')
- ->will($this->throwException(new \Sabre\DAV\Exception\ServiceUnavailable()));
-
- $propFind = new \Sabre\DAV\PropFind(
- '/dummypath',
- array(
- 'customprop',
- 'customprop2',
- 'unsetprop',
- ),
- 0
- );
-
- $this->plugin->propFind(
- '/dummypath',
- $propFind
- );
-
- $this->plugin->propFind(
- '/dummypath',
- $propFind
- );
-
- // no exception, soft fail
- $this->assertTrue(true);
- }
-
- /**
- * Test setting/getting properties
- */
- public function testSetGetPropertiesForFile() {
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/dummypath')
- ->will($this->returnValue($node));
-
- $this->applyDefaultProps();
-
- $propFind = new \Sabre\DAV\PropFind(
- '/dummypath',
- array(
- 'customprop',
- 'customprop2',
- 'unsetprop',
- ),
- 0
- );
-
- $this->plugin->propFind(
- '/dummypath',
- $propFind
- );
-
- $this->assertEquals('value1', $propFind->get('customprop'));
- $this->assertEquals('value2', $propFind->get('customprop2'));
- $this->assertEquals(array('unsetprop'), $propFind->get404Properties());
- }
-
- /**
- * Test getting properties from directory
- */
- public function testGetPropertiesForDirectory() {
- $rootNode = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory');
-
- $nodeSub = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
- ->disableOriginalConstructor()
- ->getMock();
- $nodeSub->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(456));
-
- $nodeSub->expects($this->any())
- ->method('getPath')
- ->will($this->returnValue('/dummypath/test.txt'));
-
- $rootNode->expects($this->once())
- ->method('getChildren')
- ->will($this->returnValue(array($nodeSub)));
-
- $this->tree->expects($this->at(0))
- ->method('getNodeForPath')
- ->with('/dummypath')
- ->will($this->returnValue($rootNode));
-
- $this->tree->expects($this->at(1))
- ->method('getNodeForPath')
- ->with('/dummypath/test.txt')
- ->will($this->returnValue($nodeSub));
-
- $this->tree->expects($this->at(2))
- ->method('getNodeForPath')
- ->with('/dummypath')
- ->will($this->returnValue($rootNode));
-
- $this->tree->expects($this->at(3))
- ->method('getNodeForPath')
- ->with('/dummypath/test.txt')
- ->will($this->returnValue($nodeSub));
-
- $this->applyDefaultProps('/dummypath');
- $this->applyDefaultProps('/dummypath/test.txt');
-
- $propNames = array(
- 'customprop',
- 'customprop2',
- 'unsetprop',
- );
-
- $propFindRoot = new \Sabre\DAV\PropFind(
- '/dummypath',
- $propNames,
- 1
- );
-
- $propFindSub = new \Sabre\DAV\PropFind(
- '/dummypath/test.txt',
- $propNames,
- 0
- );
-
- $this->plugin->propFind(
- '/dummypath',
- $propFindRoot
- );
-
- $this->plugin->propFind(
- '/dummypath/test.txt',
- $propFindSub
- );
-
- // TODO: find a way to assert that no additional SQL queries were
- // run while doing the second propFind
-
- $this->assertEquals('value1', $propFindRoot->get('customprop'));
- $this->assertEquals('value2', $propFindRoot->get('customprop2'));
- $this->assertEquals(array('unsetprop'), $propFindRoot->get404Properties());
-
- $this->assertEquals('value1', $propFindSub->get('customprop'));
- $this->assertEquals('value2', $propFindSub->get('customprop2'));
- $this->assertEquals(array('unsetprop'), $propFindSub->get404Properties());
- }
-
- /**
- * Test delete property
- */
- public function testDeleteProperty() {
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/dummypath')
- ->will($this->returnValue($node));
-
- $this->applyDefaultProps();
-
- $propPatch = new \Sabre\DAV\PropPatch(array(
- 'customprop' => null,
- ));
-
- $this->plugin->propPatch(
- '/dummypath',
- $propPatch
- );
-
- $propPatch->commit();
-
- $this->assertEmpty($propPatch->getRemainingMutations());
-
- $result = $propPatch->getResult();
- $this->assertEquals(204, $result['customprop']);
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/directory.php b/apps/dav/tests/unit/connector/sabre/directory.php
deleted file mode 100644
index c4ddc38b3e1..00000000000
--- a/apps/dav/tests/unit/connector/sabre/directory.php
+++ /dev/null
@@ -1,264 +0,0 @@
-<?php
-/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OCP\Files\ForbiddenException;
-
-class Directory extends \Test\TestCase {
-
- /** @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject */
- private $view;
- /** @var \OC\Files\FileInfo | \PHPUnit_Framework_MockObject_MockObject */
- private $info;
-
- protected function setUp() {
- parent::setUp();
-
- $this->view = $this->getMock('OC\Files\View', array(), array(), '', false);
- $this->info = $this->getMock('OC\Files\FileInfo', array(), array(), '', false);
- }
-
- private function getDir($path = '/') {
- $this->view->expects($this->once())
- ->method('getRelativePath')
- ->will($this->returnValue($path));
-
- $this->info->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue($path));
-
- return new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testDeleteRootFolderFails() {
- $this->info->expects($this->any())
- ->method('isDeletable')
- ->will($this->returnValue(true));
- $this->view->expects($this->never())
- ->method('rmdir');
- $dir = $this->getDir();
- $dir->delete();
- }
-
- /**
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
- */
- public function testDeleteForbidden() {
- // deletion allowed
- $this->info->expects($this->once())
- ->method('isDeletable')
- ->will($this->returnValue(true));
-
- // but fails
- $this->view->expects($this->once())
- ->method('rmdir')
- ->with('sub')
- ->willThrowException(new ForbiddenException('', true));
-
- $dir = $this->getDir('sub');
- $dir->delete();
- }
-
- /**
- *
- */
- public function testDeleteFolderWhenAllowed() {
- // deletion allowed
- $this->info->expects($this->once())
- ->method('isDeletable')
- ->will($this->returnValue(true));
-
- // but fails
- $this->view->expects($this->once())
- ->method('rmdir')
- ->with('sub')
- ->will($this->returnValue(true));
-
- $dir = $this->getDir('sub');
- $dir->delete();
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testDeleteFolderFailsWhenNotAllowed() {
- $this->info->expects($this->once())
- ->method('isDeletable')
- ->will($this->returnValue(false));
-
- $dir = $this->getDir('sub');
- $dir->delete();
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testDeleteFolderThrowsWhenDeletionFailed() {
- // deletion allowed
- $this->info->expects($this->once())
- ->method('isDeletable')
- ->will($this->returnValue(true));
-
- // but fails
- $this->view->expects($this->once())
- ->method('rmdir')
- ->with('sub')
- ->will($this->returnValue(false));
-
- $dir = $this->getDir('sub');
- $dir->delete();
- }
-
- public function testGetChildren() {
- $info1 = $this->getMockBuilder('OC\Files\FileInfo')
- ->disableOriginalConstructor()
- ->getMock();
- $info2 = $this->getMockBuilder('OC\Files\FileInfo')
- ->disableOriginalConstructor()
- ->getMock();
- $info1->expects($this->any())
- ->method('getName')
- ->will($this->returnValue('first'));
- $info1->expects($this->any())
- ->method('getEtag')
- ->will($this->returnValue('abc'));
- $info2->expects($this->any())
- ->method('getName')
- ->will($this->returnValue('second'));
- $info2->expects($this->any())
- ->method('getEtag')
- ->will($this->returnValue('def'));
-
- $this->view->expects($this->once())
- ->method('getDirectoryContent')
- ->with('')
- ->will($this->returnValue(array($info1, $info2)));
-
- $this->view->expects($this->any())
- ->method('getRelativePath')
- ->will($this->returnValue(''));
-
- $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
- $nodes = $dir->getChildren();
-
- $this->assertEquals(2, count($nodes));
-
- // calling a second time just returns the cached values,
- // does not call getDirectoryContents again
- $dir->getChildren();
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\ServiceUnavailable
- */
- public function testGetChildThrowStorageNotAvailableException() {
- $this->view->expects($this->once())
- ->method('getFileInfo')
- ->willThrowException(new \OCP\Files\StorageNotAvailableException());
-
- $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
- $dir->getChild('.');
- }
-
- /**
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath
- */
- public function testGetChildThrowInvalidPath() {
- $this->view->expects($this->once())
- ->method('verifyPath')
- ->willThrowException(new \OCP\Files\InvalidPathException());
- $this->view->expects($this->never())
- ->method('getFileInfo');
-
- $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
- $dir->getChild('.');
- }
-
- public function testGetQuotaInfoUnlimited() {
- $storage = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Quota')
- ->disableOriginalConstructor()
- ->getMock();
-
- $storage->expects($this->any())
- ->method('instanceOfStorage')
- ->will($this->returnValueMap([
- '\OC\Files\Storage\Shared' => false,
- '\OC\Files\Storage\Wrapper\Quota' => false,
- ]));
-
- $storage->expects($this->never())
- ->method('getQuota');
-
- $storage->expects($this->once())
- ->method('free_space')
- ->will($this->returnValue(800));
-
- $this->info->expects($this->once())
- ->method('getSize')
- ->will($this->returnValue(200));
-
- $this->info->expects($this->once())
- ->method('getStorage')
- ->will($this->returnValue($storage));
-
- $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
- $this->assertEquals([200, -3], $dir->getQuotaInfo()); //200 used, unlimited
- }
-
- public function testGetQuotaInfoSpecific() {
- $storage = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Quota')
- ->disableOriginalConstructor()
- ->getMock();
-
- $storage->expects($this->any())
- ->method('instanceOfStorage')
- ->will($this->returnValueMap([
- ['\OC\Files\Storage\Shared', false],
- ['\OC\Files\Storage\Wrapper\Quota', true],
- ]));
-
- $storage->expects($this->once())
- ->method('getQuota')
- ->will($this->returnValue(1000));
-
- $storage->expects($this->once())
- ->method('free_space')
- ->will($this->returnValue(800));
-
- $this->info->expects($this->once())
- ->method('getSize')
- ->will($this->returnValue(200));
-
- $this->info->expects($this->once())
- ->method('getStorage')
- ->will($this->returnValue($storage));
-
- $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
- $this->assertEquals([200, 800], $dir->getQuotaInfo()); //200 used, 800 free
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php b/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php
deleted file mode 100644
index 36ea97df9f7..00000000000
--- a/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception;
-
-use OCA\DAV\Connector\Sabre\Exception\Forbidden;
-
-class ForbiddenTest extends \Test\TestCase {
-
- public function testSerialization() {
-
- // create xml doc
- $DOM = new \DOMDocument('1.0','utf-8');
- $DOM->formatOutput = true;
- $error = $DOM->createElementNS('DAV:','d:error');
- $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV);
- $DOM->appendChild($error);
-
- // serialize the exception
- $message = "1234567890";
- $retry = false;
- $expectedXml = <<<EOD
-<?xml version="1.0" encoding="utf-8"?>
-<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns">
- <o:retry xmlns:o="o:">false</o:retry>
- <o:reason xmlns:o="o:">1234567890</o:reason>
-</d:error>
-
-EOD;
-
- $ex = new Forbidden($message, $retry);
- $server = $this->getMock('Sabre\DAV\Server');
- $ex->serialize($server, $error);
-
- // assert
- $xml = $DOM->saveXML();
- $this->assertEquals($expectedXml, $xml);
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php
deleted file mode 100644
index 431a0484d65..00000000000
--- a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception;
-
-use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
-
-class InvalidPathTest extends \Test\TestCase {
-
- public function testSerialization() {
-
- // create xml doc
- $DOM = new \DOMDocument('1.0','utf-8');
- $DOM->formatOutput = true;
- $error = $DOM->createElementNS('DAV:','d:error');
- $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV);
- $DOM->appendChild($error);
-
- // serialize the exception
- $message = "1234567890";
- $retry = false;
- $expectedXml = <<<EOD
-<?xml version="1.0" encoding="utf-8"?>
-<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns">
- <o:retry xmlns:o="o:">false</o:retry>
- <o:reason xmlns:o="o:">1234567890</o:reason>
-</d:error>
-
-EOD;
-
- $ex = new InvalidPath($message, $retry);
- $server = $this->getMock('Sabre\DAV\Server');
- $ex->serialize($server, $error);
-
- // assert
- $xml = $DOM->saveXML();
- $this->assertEquals($expectedXml, $xml);
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php b/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php
deleted file mode 100644
index b76285be336..00000000000
--- a/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
-use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin as PluginToTest;
-use OC\Log;
-use OCP\ILogger;
-use PHPUnit_Framework_MockObject_MockObject;
-use Sabre\DAV\Exception\NotFound;
-use Sabre\DAV\Server;
-use Test\TestCase;
-
-class TestLogger extends Log {
- public $message;
- public $level;
-
- public function __construct($logger = null) {
- //disable original constructor
- }
-
- public function log($level, $message, array $context = array()) {
- $this->level = $level;
- $this->message = $message;
- }
-}
-
-class ExceptionLoggerPlugin extends TestCase {
-
- /** @var Server */
- private $server;
-
- /** @var PluginToTest */
- private $plugin;
-
- /** @var TestLogger | PHPUnit_Framework_MockObject_MockObject */
- private $logger;
-
- private function init() {
- $this->server = new Server();
- $this->logger = new TestLogger();
- $this->plugin = new PluginToTest('unit-test', $this->logger);
- $this->plugin->initialize($this->server);
- }
-
- /**
- * @dataProvider providesExceptions
- */
- public function testLogging($expectedLogLevel, $expectedMessage, $exception) {
- $this->init();
- $this->plugin->logException($exception);
-
- $this->assertEquals($expectedLogLevel, $this->logger->level);
- $this->assertStringStartsWith('Exception: {"Message":"' . $expectedMessage, $this->logger->message);
- }
-
- public function providesExceptions() {
- return [
- [0, 'HTTP\/1.1 404 Not Found', new NotFound()],
- [4, 'HTTP\/1.1 400 This path leads to nowhere', new InvalidPath('This path leads to nowhere')]
- ];
- }
-
-}
diff --git a/apps/dav/tests/unit/connector/sabre/file.php b/apps/dav/tests/unit/connector/sabre/file.php
deleted file mode 100644
index eab7ece159c..00000000000
--- a/apps/dav/tests/unit/connector/sabre/file.php
+++ /dev/null
@@ -1,987 +0,0 @@
-<?php
-/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OC\Files\Storage\Local;
-use OCP\Files\ForbiddenException;
-use Test\HookHelper;
-use OC\Files\Filesystem;
-use OCP\Lock\ILockingProvider;
-
-/**
- * Class File
- *
- * @group DB
- *
- * @package Test\Connector\Sabre
- */
-class File extends \Test\TestCase {
-
- /**
- * @var string
- */
- private $user;
-
- public function setUp() {
- parent::setUp();
-
- \OC_Hook::clear();
-
- $this->user = $this->getUniqueID('user_');
- $userManager = \OC::$server->getUserManager();
- $userManager->createUser($this->user, 'pass');
-
- $this->loginAsUser($this->user);
- }
-
- public function tearDown() {
- $userManager = \OC::$server->getUserManager();
- $userManager->get($this->user)->delete();
- unset($_SERVER['HTTP_OC_CHUNKED']);
-
- parent::tearDown();
- }
-
- private function getMockStorage() {
- $storage = $this->getMock('\OCP\Files\Storage');
- $storage->expects($this->any())
- ->method('getId')
- ->will($this->returnValue('home::someuser'));
- return $storage;
- }
-
- /**
- * @param string $string
- */
- private function getStream($string) {
- $stream = fopen('php://temp', 'r+');
- fwrite($stream, $string);
- fseek($stream, 0);
- return $stream;
- }
-
-
- public function fopenFailuresProvider() {
- return [
- [
- // return false
- null,
- '\Sabre\Dav\Exception',
- false
- ],
- [
- new \OCP\Files\NotPermittedException(),
- 'Sabre\DAV\Exception\Forbidden'
- ],
- [
- new \OCP\Files\EntityTooLargeException(),
- 'OCA\DAV\Connector\Sabre\Exception\EntityTooLarge'
- ],
- [
- new \OCP\Files\InvalidContentException(),
- 'OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType'
- ],
- [
- new \OCP\Files\InvalidPathException(),
- 'Sabre\DAV\Exception\Forbidden'
- ],
- [
- new \OCP\Files\ForbiddenException('', true),
- 'OCA\DAV\Connector\Sabre\Exception\Forbidden'
- ],
- [
- new \OCP\Files\LockNotAcquiredException('/test.txt', 1),
- 'OCA\DAV\Connector\Sabre\Exception\FileLocked'
- ],
- [
- new \OCP\Lock\LockedException('/test.txt'),
- 'OCA\DAV\Connector\Sabre\Exception\FileLocked'
- ],
- [
- new \OCP\Encryption\Exceptions\GenericEncryptionException(),
- 'Sabre\DAV\Exception\ServiceUnavailable'
- ],
- [
- new \OCP\Files\StorageNotAvailableException(),
- 'Sabre\DAV\Exception\ServiceUnavailable'
- ],
- [
- new \Sabre\DAV\Exception('Generic sabre exception'),
- 'Sabre\DAV\Exception',
- false
- ],
- [
- new \Exception('Generic exception'),
- 'Sabre\DAV\Exception'
- ],
- ];
- }
-
- /**
- * @dataProvider fopenFailuresProvider
- */
- public function testSimplePutFails($thrownException, $expectedException, $checkPreviousClass = true) {
- // setup
- $storage = $this->getMock(
- '\OC\Files\Storage\Local',
- ['fopen'],
- [['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]]
- );
- \OC\Files\Filesystem::mount($storage, [], $this->user . '/');
- $view = $this->getMock('\OC\Files\View', array('getRelativePath', 'resolvePath'), array());
- $view->expects($this->atLeastOnce())
- ->method('resolvePath')
- ->will($this->returnCallback(
- function ($path) use ($storage) {
- return [$storage, $path];
- }
- ));
-
- if ($thrownException !== null) {
- $storage->expects($this->once())
- ->method('fopen')
- ->will($this->throwException($thrownException));
- } else {
- $storage->expects($this->once())
- ->method('fopen')
- ->will($this->returnValue(false));
- }
-
- $view->expects($this->any())
- ->method('getRelativePath')
- ->will($this->returnArgument(0));
-
- $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $caughtException = null;
- try {
- $file->put('test data');
- } catch (\Exception $e) {
- $caughtException = $e;
- }
-
- $this->assertInstanceOf($expectedException, $caughtException);
- if ($checkPreviousClass) {
- $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious());
- }
-
- $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
- }
-
- /**
- * Test putting a file using chunking
- *
- * @dataProvider fopenFailuresProvider
- */
- public function testChunkedPutFails($thrownException, $expectedException, $checkPreviousClass = false) {
- // setup
- $storage = $this->getMock(
- '\OC\Files\Storage\Local',
- ['fopen'],
- [['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]]
- );
- \OC\Files\Filesystem::mount($storage, [], $this->user . '/');
- $view = $this->getMock('\OC\Files\View', ['getRelativePath', 'resolvePath'], []);
- $view->expects($this->atLeastOnce())
- ->method('resolvePath')
- ->will($this->returnCallback(
- function ($path) use ($storage) {
- return [$storage, $path];
- }
- ));
-
- if ($thrownException !== null) {
- $storage->expects($this->once())
- ->method('fopen')
- ->will($this->throwException($thrownException));
- } else {
- $storage->expects($this->once())
- ->method('fopen')
- ->will($this->returnValue(false));
- }
-
- $view->expects($this->any())
- ->method('getRelativePath')
- ->will($this->returnArgument(0));
-
- $_SERVER['HTTP_OC_CHUNKED'] = true;
-
- $info = new \OC\Files\FileInfo('/test.txt-chunking-12345-2-0', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // put first chunk
- $file->acquireLock(ILockingProvider::LOCK_SHARED);
- $this->assertNull($file->put('test data one'));
- $file->releaseLock(ILockingProvider::LOCK_SHARED);
-
- $info = new \OC\Files\FileInfo('/test.txt-chunking-12345-2-1', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $caughtException = null;
- try {
- // last chunk
- $file->acquireLock(ILockingProvider::LOCK_SHARED);
- $file->put('test data two');
- $file->releaseLock(ILockingProvider::LOCK_SHARED);
- } catch (\Exception $e) {
- $caughtException = $e;
- }
-
- $this->assertInstanceOf($expectedException, $caughtException);
- if ($checkPreviousClass) {
- $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious());
- }
-
- $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
- }
-
- /**
- * Simulate putting a file to the given path.
- *
- * @param string $path path to put the file into
- * @param string $viewRoot root to use for the view
- *
- * @return null|string of the PUT operaiton which is usually the etag
- */
- private function doPut($path, $viewRoot = null) {
- $view = \OC\Files\Filesystem::getView();
- if (!is_null($viewRoot)) {
- $view = new \OC\Files\View($viewRoot);
- } else {
- $viewRoot = '/' . $this->user . '/files';
- }
-
- $info = new \OC\Files\FileInfo(
- $viewRoot . '/' . ltrim($path, '/'),
- $this->getMockStorage(),
- null,
- ['permissions' => \OCP\Constants::PERMISSION_ALL],
- null
- );
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // beforeMethod locks
- $view->lockFile($path, ILockingProvider::LOCK_SHARED);
-
- $result = $file->put($this->getStream('test data'));
-
- // afterMethod unlocks
- $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
-
- return $result;
- }
-
- /**
- * Test putting a single file
- */
- public function testPutSingleFile() {
- $this->assertNotEmpty($this->doPut('/foo.txt'));
- }
-
- /**
- * Test putting a file using chunking
- */
- public function testChunkedPut() {
- $_SERVER['HTTP_OC_CHUNKED'] = true;
- $this->assertNull($this->doPut('/test.txt-chunking-12345-2-0'));
- $this->assertNotEmpty($this->doPut('/test.txt-chunking-12345-2-1'));
- }
-
- /**
- * Test that putting a file triggers create hooks
- */
- public function testPutSingleFileTriggersHooks() {
- HookHelper::setUpHooks();
-
- $this->assertNotEmpty($this->doPut('/foo.txt'));
-
- $this->assertCount(4, HookHelper::$hookCalls);
- $this->assertHookCall(
- HookHelper::$hookCalls[0],
- Filesystem::signal_create,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[1],
- Filesystem::signal_write,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[2],
- Filesystem::signal_post_create,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[3],
- Filesystem::signal_post_write,
- '/foo.txt'
- );
- }
-
- /**
- * Test that putting a file triggers update hooks
- */
- public function testPutOverwriteFileTriggersHooks() {
- $view = \OC\Files\Filesystem::getView();
- $view->file_put_contents('/foo.txt', 'some content that will be replaced');
-
- HookHelper::setUpHooks();
-
- $this->assertNotEmpty($this->doPut('/foo.txt'));
-
- $this->assertCount(4, HookHelper::$hookCalls);
- $this->assertHookCall(
- HookHelper::$hookCalls[0],
- Filesystem::signal_update,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[1],
- Filesystem::signal_write,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[2],
- Filesystem::signal_post_update,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[3],
- Filesystem::signal_post_write,
- '/foo.txt'
- );
- }
-
- /**
- * Test that putting a file triggers hooks with the correct path
- * if the passed view was chrooted (can happen with public webdav
- * where the root is the share root)
- */
- public function testPutSingleFileTriggersHooksDifferentRoot() {
- $view = \OC\Files\Filesystem::getView();
- $view->mkdir('noderoot');
-
- HookHelper::setUpHooks();
-
- // happens with public webdav where the view root is the share root
- $this->assertNotEmpty($this->doPut('/foo.txt', '/' . $this->user . '/files/noderoot'));
-
- $this->assertCount(4, HookHelper::$hookCalls);
- $this->assertHookCall(
- HookHelper::$hookCalls[0],
- Filesystem::signal_create,
- '/noderoot/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[1],
- Filesystem::signal_write,
- '/noderoot/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[2],
- Filesystem::signal_post_create,
- '/noderoot/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[3],
- Filesystem::signal_post_write,
- '/noderoot/foo.txt'
- );
- }
-
- /**
- * Test that putting a file with chunks triggers create hooks
- */
- public function testPutChunkedFileTriggersHooks() {
- HookHelper::setUpHooks();
-
- $_SERVER['HTTP_OC_CHUNKED'] = true;
- $this->assertNull($this->doPut('/foo.txt-chunking-12345-2-0'));
- $this->assertNotEmpty($this->doPut('/foo.txt-chunking-12345-2-1'));
-
- $this->assertCount(4, HookHelper::$hookCalls);
- $this->assertHookCall(
- HookHelper::$hookCalls[0],
- Filesystem::signal_create,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[1],
- Filesystem::signal_write,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[2],
- Filesystem::signal_post_create,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[3],
- Filesystem::signal_post_write,
- '/foo.txt'
- );
- }
-
- /**
- * Test that putting a chunked file triggers update hooks
- */
- public function testPutOverwriteChunkedFileTriggersHooks() {
- $view = \OC\Files\Filesystem::getView();
- $view->file_put_contents('/foo.txt', 'some content that will be replaced');
-
- HookHelper::setUpHooks();
-
- $_SERVER['HTTP_OC_CHUNKED'] = true;
- $this->assertNull($this->doPut('/foo.txt-chunking-12345-2-0'));
- $this->assertNotEmpty($this->doPut('/foo.txt-chunking-12345-2-1'));
-
- $this->assertCount(4, HookHelper::$hookCalls);
- $this->assertHookCall(
- HookHelper::$hookCalls[0],
- Filesystem::signal_update,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[1],
- Filesystem::signal_write,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[2],
- Filesystem::signal_post_update,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[3],
- Filesystem::signal_post_write,
- '/foo.txt'
- );
- }
-
- public static function cancellingHook($params) {
- self::$hookCalls[] = array(
- 'signal' => Filesystem::signal_post_create,
- 'params' => $params
- );
- }
-
- /**
- * Test put file with cancelled hook
- */
- public function testPutSingleFileCancelPreHook() {
- \OCP\Util::connectHook(
- Filesystem::CLASSNAME,
- Filesystem::signal_create,
- '\Test\HookHelper',
- 'cancellingCallback'
- );
-
- // action
- $thrown = false;
- try {
- $this->doPut('/foo.txt');
- } catch (\Sabre\DAV\Exception $e) {
- $thrown = true;
- }
-
- $this->assertTrue($thrown);
- $this->assertEmpty($this->listPartFiles(), 'No stray part files');
- }
-
- /**
- * Test exception when the uploaded size did not match
- */
- public function testSimplePutFailsSizeCheck() {
- // setup
- $view = $this->getMock('\OC\Files\View',
- array('rename', 'getRelativePath', 'filesize'));
- $view->expects($this->any())
- ->method('rename')
- ->withAnyParameters()
- ->will($this->returnValue(false));
- $view->expects($this->any())
- ->method('getRelativePath')
- ->will($this->returnArgument(0));
-
- $view->expects($this->any())
- ->method('filesize')
- ->will($this->returnValue(123456));
-
- $_SERVER['CONTENT_LENGTH'] = 123456;
- $_SERVER['REQUEST_METHOD'] = 'PUT';
-
- $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $thrown = false;
- try {
- // beforeMethod locks
- $file->acquireLock(ILockingProvider::LOCK_SHARED);
-
- $file->put($this->getStream('test data'));
-
- // afterMethod unlocks
- $file->releaseLock(ILockingProvider::LOCK_SHARED);
- } catch (\Sabre\DAV\Exception\BadRequest $e) {
- $thrown = true;
- }
-
- $this->assertTrue($thrown);
- $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
- }
-
- /**
- * Test exception during final rename in simple upload mode
- */
- public function testSimplePutFailsMoveFromStorage() {
- $view = new \OC\Files\View('/' . $this->user . '/files');
-
- // simulate situation where the target file is locked
- $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE);
-
- $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $thrown = false;
- try {
- // beforeMethod locks
- $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
-
- $file->put($this->getStream('test data'));
-
- // afterMethod unlocks
- $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
- } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
- $thrown = true;
- }
-
- $this->assertTrue($thrown);
- $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
- }
-
- /**
- * Test exception during final rename in chunk upload mode
- */
- public function testChunkedPutFailsFinalRename() {
- $view = new \OC\Files\View('/' . $this->user . '/files');
-
- // simulate situation where the target file is locked
- $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE);
-
- $_SERVER['HTTP_OC_CHUNKED'] = true;
-
- $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt-chunking-12345-2-0', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
- $file->acquireLock(ILockingProvider::LOCK_SHARED);
- $this->assertNull($file->put('test data one'));
- $file->releaseLock(ILockingProvider::LOCK_SHARED);
-
- $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt-chunking-12345-2-1', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $thrown = false;
- try {
- $file->acquireLock(ILockingProvider::LOCK_SHARED);
- $file->put($this->getStream('test data'));
- $file->releaseLock(ILockingProvider::LOCK_SHARED);
- } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
- $thrown = true;
- }
-
- $this->assertTrue($thrown);
- $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
- }
-
- /**
- * Test put file with invalid chars
- */
- public function testSimplePutInvalidChars() {
- // setup
- $view = $this->getMock('\OC\Files\View', array('getRelativePath'));
- $view->expects($this->any())
- ->method('getRelativePath')
- ->will($this->returnArgument(0));
-
- $info = new \OC\Files\FileInfo('/*', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $thrown = false;
- try {
- // beforeMethod locks
- $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
-
- $file->put($this->getStream('test data'));
-
- // afterMethod unlocks
- $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
- } catch (\OCA\DAV\Connector\Sabre\Exception\InvalidPath $e) {
- $thrown = true;
- }
-
- $this->assertTrue($thrown);
- $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
- }
-
- /**
- * Test setting name with setName() with invalid chars
- *
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath
- */
- public function testSetNameInvalidChars() {
- // setup
- $view = $this->getMock('\OC\Files\View', array('getRelativePath'));
-
- $view->expects($this->any())
- ->method('getRelativePath')
- ->will($this->returnArgument(0));
-
- $info = new \OC\Files\FileInfo('/*', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
- $file->setName('/super*star.txt');
- }
-
- /**
- */
- public function testUploadAbort() {
- // setup
- $view = $this->getMock('\OC\Files\View',
- array('rename', 'getRelativePath', 'filesize'));
- $view->expects($this->any())
- ->method('rename')
- ->withAnyParameters()
- ->will($this->returnValue(false));
- $view->expects($this->any())
- ->method('getRelativePath')
- ->will($this->returnArgument(0));
- $view->expects($this->any())
- ->method('filesize')
- ->will($this->returnValue(123456));
-
- $_SERVER['CONTENT_LENGTH'] = 12345;
- $_SERVER['REQUEST_METHOD'] = 'PUT';
-
- $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $thrown = false;
- try {
- // beforeMethod locks
- $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
-
- $file->put($this->getStream('test data'));
-
- // afterMethod unlocks
- $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
- } catch (\Sabre\DAV\Exception\BadRequest $e) {
- $thrown = true;
- }
-
- $this->assertTrue($thrown);
- $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
- }
-
- /**
- *
- */
- public function testDeleteWhenAllowed() {
- // setup
- $view = $this->getMock('\OC\Files\View',
- array());
-
- $view->expects($this->once())
- ->method('unlink')
- ->will($this->returnValue(true));
-
- $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $file->delete();
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testDeleteThrowsWhenDeletionNotAllowed() {
- // setup
- $view = $this->getMock('\OC\Files\View',
- array());
-
- $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array(
- 'permissions' => 0
- ), null);
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $file->delete();
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testDeleteThrowsWhenDeletionFailed() {
- // setup
- $view = $this->getMock('\OC\Files\View',
- array());
-
- // but fails
- $view->expects($this->once())
- ->method('unlink')
- ->will($this->returnValue(false));
-
- $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $file->delete();
- }
-
- /**
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
- */
- public function testDeleteThrowsWhenDeletionThrows() {
- // setup
- $view = $this->getMock('\OC\Files\View',
- array());
-
- // but fails
- $view->expects($this->once())
- ->method('unlink')
- ->willThrowException(new ForbiddenException('', true));
-
- $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $file->delete();
- }
-
- /**
- * Asserts hook call
- *
- * @param array $callData hook call data to check
- * @param string $signal signal name
- * @param string $hookPath hook path
- */
- protected function assertHookCall($callData, $signal, $hookPath) {
- $this->assertEquals($signal, $callData['signal']);
- $params = $callData['params'];
- $this->assertEquals(
- $hookPath,
- $params[Filesystem::signal_param_path]
- );
- }
-
- /**
- * Test whether locks are set before and after the operation
- */
- public function testPutLocking() {
- $view = new \OC\Files\View('/' . $this->user . '/files/');
-
- $path = 'test-locking.txt';
- $info = new \OC\Files\FileInfo(
- '/' . $this->user . '/files/' . $path,
- $this->getMockStorage(),
- null,
- ['permissions' => \OCP\Constants::PERMISSION_ALL],
- null
- );
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- $this->assertFalse(
- $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED),
- 'File unlocked before put'
- );
- $this->assertFalse(
- $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE),
- 'File unlocked before put'
- );
-
- $wasLockedPre = false;
- $wasLockedPost = false;
- $eventHandler = $this->getMockBuilder('\stdclass')
- ->setMethods(['writeCallback', 'postWriteCallback'])
- ->getMock();
-
- // both pre and post hooks might need access to the file,
- // so only shared lock is acceptable
- $eventHandler->expects($this->once())
- ->method('writeCallback')
- ->will($this->returnCallback(
- function () use ($view, $path, &$wasLockedPre) {
- $wasLockedPre = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
- $wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
- }
- ));
- $eventHandler->expects($this->once())
- ->method('postWriteCallback')
- ->will($this->returnCallback(
- function () use ($view, $path, &$wasLockedPost) {
- $wasLockedPost = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
- $wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
- }
- ));
-
- \OCP\Util::connectHook(
- Filesystem::CLASSNAME,
- Filesystem::signal_write,
- $eventHandler,
- 'writeCallback'
- );
- \OCP\Util::connectHook(
- Filesystem::CLASSNAME,
- Filesystem::signal_post_write,
- $eventHandler,
- 'postWriteCallback'
- );
-
- // beforeMethod locks
- $view->lockFile($path, ILockingProvider::LOCK_SHARED);
-
- $this->assertNotEmpty($file->put($this->getStream('test data')));
-
- // afterMethod unlocks
- $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
-
- $this->assertTrue($wasLockedPre, 'File was locked during pre-hooks');
- $this->assertTrue($wasLockedPost, 'File was locked during post-hooks');
-
- $this->assertFalse(
- $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED),
- 'File unlocked after put'
- );
- $this->assertFalse(
- $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE),
- 'File unlocked after put'
- );
- }
-
- /**
- * Returns part files in the given path
- *
- * @param \OC\Files\View view which root is the current user's "files" folder
- * @param string $path path for which to list part files
- *
- * @return array list of part files
- */
- private function listPartFiles(\OC\Files\View $userView = null, $path = '') {
- if ($userView === null) {
- $userView = \OC\Files\Filesystem::getView();
- }
- $files = [];
- list($storage, $internalPath) = $userView->resolvePath($path);
- if($storage instanceof Local) {
- $realPath = $storage->getSourcePath($internalPath);
- $dh = opendir($realPath);
- while (($file = readdir($dh)) !== false) {
- if (substr($file, strlen($file) - 5, 5) === '.part') {
- $files[] = $file;
- }
- }
- closedir($dh);
- }
- return $files;
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\ServiceUnavailable
- */
- public function testGetFopenFails() {
- $view = $this->getMock('\OC\Files\View', ['fopen'], array());
- $view->expects($this->atLeastOnce())
- ->method('fopen')
- ->will($this->returnValue(false));
-
- $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- $file->get();
- }
-
- /**
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
- */
- public function testGetFopenThrows() {
- $view = $this->getMock('\OC\Files\View', ['fopen'], array());
- $view->expects($this->atLeastOnce())
- ->method('fopen')
- ->willThrowException(new ForbiddenException('', true));
-
- $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array(
- 'permissions' => \OCP\Constants::PERMISSION_ALL
- ), null);
-
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- $file->get();
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/filesplugin.php b/apps/dav/tests/unit/connector/sabre/filesplugin.php
deleted file mode 100644
index e88066a12da..00000000000
--- a/apps/dav/tests/unit/connector/sabre/filesplugin.php
+++ /dev/null
@@ -1,419 +0,0 @@
-<?php
-/**
- * @author Roeland Jago Douma <rullzer@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OCP\Files\StorageNotAvailableException;
-
-/**
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- * This file is licensed under the Affero General Public License version 3 or
- * later.
- * See the COPYING-README file.
- */
-class FilesPlugin extends \Test\TestCase {
- const GETETAG_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::GETETAG_PROPERTYNAME;
- const FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::FILEID_PROPERTYNAME;
- const INTERNAL_FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::INTERNAL_FILEID_PROPERTYNAME;
- const SIZE_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::SIZE_PROPERTYNAME;
- const PERMISSIONS_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::PERMISSIONS_PROPERTYNAME;
- const LASTMODIFIED_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::LASTMODIFIED_PROPERTYNAME;
- const DOWNLOADURL_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::DOWNLOADURL_PROPERTYNAME;
- const OWNER_ID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::OWNER_ID_PROPERTYNAME;
- const OWNER_DISPLAY_NAME_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME;
-
- /**
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var \Sabre\DAV\Tree
- */
- private $tree;
-
- /**
- * @var \OCA\DAV\Connector\Sabre\FilesPlugin
- */
- private $plugin;
-
- /**
- * @var \OC\Files\View
- */
- private $view;
-
- public function setUp() {
- parent::setUp();
- $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
- ->disableOriginalConstructor()
- ->getMock();
- $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
- ->disableOriginalConstructor()
- ->getMock();
- $this->view = $this->getMockBuilder('\OC\Files\View')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->plugin = new \OCA\DAV\Connector\Sabre\FilesPlugin($this->tree, $this->view);
- $this->plugin->initialize($this->server);
- }
-
- /**
- * @param string $class
- */
- private function createTestNode($class) {
- $node = $this->getMockBuilder($class)
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(123));
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/dummypath')
- ->will($this->returnValue($node));
-
- $node->expects($this->any())
- ->method('getFileId')
- ->will($this->returnValue('00000123instanceid'));
- $node->expects($this->any())
- ->method('getInternalFileId')
- ->will($this->returnValue('123'));
- $node->expects($this->any())
- ->method('getEtag')
- ->will($this->returnValue('"abc"'));
- $node->expects($this->any())
- ->method('getDavPermissions')
- ->will($this->returnValue('DWCKMSR'));
-
- return $node;
- }
-
- public function testGetPropertiesForFile() {
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
-
- $propFind = new \Sabre\DAV\PropFind(
- '/dummyPath',
- array(
- self::GETETAG_PROPERTYNAME,
- self::FILEID_PROPERTYNAME,
- self::INTERNAL_FILEID_PROPERTYNAME,
- self::SIZE_PROPERTYNAME,
- self::PERMISSIONS_PROPERTYNAME,
- self::DOWNLOADURL_PROPERTYNAME,
- self::OWNER_ID_PROPERTYNAME,
- self::OWNER_DISPLAY_NAME_PROPERTYNAME
- ),
- 0
- );
-
- $user = $this->getMockBuilder('\OC\User\User')
- ->disableOriginalConstructor()->getMock();
- $user
- ->expects($this->once())
- ->method('getUID')
- ->will($this->returnValue('foo'));
- $user
- ->expects($this->once())
- ->method('getDisplayName')
- ->will($this->returnValue('M. Foo'));
-
- $node->expects($this->once())
- ->method('getDirectDownload')
- ->will($this->returnValue(array('url' => 'http://example.com/')));
- $node->expects($this->exactly(2))
- ->method('getOwner')
- ->will($this->returnValue($user));
- $node->expects($this->never())
- ->method('getSize');
-
- $this->plugin->handleGetProperties(
- $propFind,
- $node
- );
-
- $this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME));
- $this->assertEquals('00000123instanceid', $propFind->get(self::FILEID_PROPERTYNAME));
- $this->assertEquals('123', $propFind->get(self::INTERNAL_FILEID_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::SIZE_PROPERTYNAME));
- $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
- $this->assertEquals('http://example.com/', $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
- $this->assertEquals('foo', $propFind->get(self::OWNER_ID_PROPERTYNAME));
- $this->assertEquals('M. Foo', $propFind->get(self::OWNER_DISPLAY_NAME_PROPERTYNAME));
- $this->assertEquals(array(self::SIZE_PROPERTYNAME), $propFind->get404Properties());
- }
-
- public function testGetPropertiesForFileHome() {
- $node = $this->createTestNode('\OCA\DAV\Files\FilesHome');
-
- $propFind = new \Sabre\DAV\PropFind(
- '/dummyPath',
- array(
- self::GETETAG_PROPERTYNAME,
- self::FILEID_PROPERTYNAME,
- self::INTERNAL_FILEID_PROPERTYNAME,
- self::SIZE_PROPERTYNAME,
- self::PERMISSIONS_PROPERTYNAME,
- self::DOWNLOADURL_PROPERTYNAME,
- self::OWNER_ID_PROPERTYNAME,
- self::OWNER_DISPLAY_NAME_PROPERTYNAME
- ),
- 0
- );
-
- $user = $this->getMockBuilder('\OC\User\User')
- ->disableOriginalConstructor()->getMock();
- $user->expects($this->never())->method('getUID');
- $user->expects($this->never())->method('getDisplayName');
- $node->expects($this->never())->method('getDirectDownload');
- $node->expects($this->never())->method('getOwner');
- $node->expects($this->never())->method('getSize');
-
- $this->plugin->handleGetProperties(
- $propFind,
- $node
- );
-
- $this->assertEquals(null, $propFind->get(self::GETETAG_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::FILEID_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::INTERNAL_FILEID_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::SIZE_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::PERMISSIONS_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::OWNER_ID_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::OWNER_DISPLAY_NAME_PROPERTYNAME));
- $this->assertEquals(['{DAV:}getetag',
- '{http://owncloud.org/ns}id',
- '{http://owncloud.org/ns}fileid',
- '{http://owncloud.org/ns}size',
- '{http://owncloud.org/ns}permissions',
- '{http://owncloud.org/ns}downloadURL',
- '{http://owncloud.org/ns}owner-id',
- '{http://owncloud.org/ns}owner-display-name'
- ], $propFind->get404Properties());
- }
-
- public function testGetPropertiesStorageNotAvailable() {
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
-
- $propFind = new \Sabre\DAV\PropFind(
- '/dummyPath',
- array(
- self::DOWNLOADURL_PROPERTYNAME,
- ),
- 0
- );
-
- $node->expects($this->once())
- ->method('getDirectDownload')
- ->will($this->throwException(new StorageNotAvailableException()));
-
- $this->plugin->handleGetProperties(
- $propFind,
- $node
- );
-
- $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
- }
-
- public function testGetPublicPermissions() {
- $this->plugin = new \OCA\DAV\Connector\Sabre\FilesPlugin($this->tree, $this->view, true);
- $this->plugin->initialize($this->server);
-
- $propFind = new \Sabre\DAV\PropFind(
- '/dummyPath',
- [
- self::PERMISSIONS_PROPERTYNAME,
- ],
- 0
- );
-
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
- $node->expects($this->any())
- ->method('getDavPermissions')
- ->will($this->returnValue('DWCKMSR'));
-
- $this->plugin->handleGetProperties(
- $propFind,
- $node
- );
-
- $this->assertEquals('DWCKR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
- }
-
- public function testGetPropertiesForDirectory() {
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory');
-
- $propFind = new \Sabre\DAV\PropFind(
- '/dummyPath',
- array(
- self::GETETAG_PROPERTYNAME,
- self::FILEID_PROPERTYNAME,
- self::SIZE_PROPERTYNAME,
- self::PERMISSIONS_PROPERTYNAME,
- self::DOWNLOADURL_PROPERTYNAME,
- ),
- 0
- );
-
- $node->expects($this->never())
- ->method('getDirectDownload');
- $node->expects($this->once())
- ->method('getSize')
- ->will($this->returnValue(1025));
-
- $this->plugin->handleGetProperties(
- $propFind,
- $node
- );
-
- $this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME));
- $this->assertEquals('00000123instanceid', $propFind->get(self::FILEID_PROPERTYNAME));
- $this->assertEquals(1025, $propFind->get(self::SIZE_PROPERTYNAME));
- $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
- $this->assertEquals(array(self::DOWNLOADURL_PROPERTYNAME), $propFind->get404Properties());
- }
-
- public function testUpdateProps() {
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
-
- $testDate = 'Fri, 13 Feb 2015 00:01:02 GMT';
-
- $node->expects($this->once())
- ->method('touch')
- ->with($testDate);
-
- $node->expects($this->once())
- ->method('setEtag')
- ->with('newetag')
- ->will($this->returnValue(true));
-
- // properties to set
- $propPatch = new \Sabre\DAV\PropPatch(array(
- self::GETETAG_PROPERTYNAME => 'newetag',
- self::LASTMODIFIED_PROPERTYNAME => $testDate
- ));
-
- $this->plugin->handleUpdateProperties(
- '/dummypath',
- $propPatch
- );
-
- $propPatch->commit();
-
- $this->assertEmpty($propPatch->getRemainingMutations());
-
- $result = $propPatch->getResult();
- $this->assertEquals(200, $result[self::LASTMODIFIED_PROPERTYNAME]);
- $this->assertEquals(200, $result[self::GETETAG_PROPERTYNAME]);
- }
-
- public function testUpdatePropsForbidden() {
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
-
- $propPatch = new \Sabre\DAV\PropPatch(array(
- self::OWNER_ID_PROPERTYNAME => 'user2',
- self::OWNER_DISPLAY_NAME_PROPERTYNAME => 'User Two',
- self::FILEID_PROPERTYNAME => 12345,
- self::PERMISSIONS_PROPERTYNAME => 'C',
- self::SIZE_PROPERTYNAME => 123,
- self::DOWNLOADURL_PROPERTYNAME => 'http://example.com/',
- ));
-
- $this->plugin->handleUpdateProperties(
- '/dummypath',
- $propPatch
- );
-
- $propPatch->commit();
-
- $this->assertEmpty($propPatch->getRemainingMutations());
-
- $result = $propPatch->getResult();
- $this->assertEquals(403, $result[self::OWNER_ID_PROPERTYNAME]);
- $this->assertEquals(403, $result[self::OWNER_DISPLAY_NAME_PROPERTYNAME]);
- $this->assertEquals(403, $result[self::FILEID_PROPERTYNAME]);
- $this->assertEquals(403, $result[self::PERMISSIONS_PROPERTYNAME]);
- $this->assertEquals(403, $result[self::SIZE_PROPERTYNAME]);
- $this->assertEquals(403, $result[self::DOWNLOADURL_PROPERTYNAME]);
- }
-
- /**
- * Testcase from https://github.com/owncloud/core/issues/5251
- *
- * |-FolderA
- * |-text.txt
- * |-test.txt
- *
- * FolderA is an incoming shared folder and there are no delete permissions.
- * Thus moving /FolderA/test.txt to /test.txt should fail already on that check
- *
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage FolderA/test.txt cannot be deleted
- */
- public function testMoveSrcNotDeletable() {
- $fileInfoFolderATestTXT = $this->getMockBuilder('\OCP\Files\FileInfo')
- ->disableOriginalConstructor()
- ->getMock();
- $fileInfoFolderATestTXT->expects($this->once())
- ->method('isDeletable')
- ->willReturn(false);
-
- $this->view->expects($this->once())
- ->method('getFileInfo')
- ->with('FolderA/test.txt')
- ->willReturn($fileInfoFolderATestTXT);
-
- $this->plugin->checkMove('FolderA/test.txt', 'test.txt');
- }
-
- public function testMoveSrcDeletable() {
- $fileInfoFolderATestTXT = $this->getMockBuilder('\OCP\Files\FileInfo')
- ->disableOriginalConstructor()
- ->getMock();
- $fileInfoFolderATestTXT->expects($this->once())
- ->method('isDeletable')
- ->willReturn(true);
-
- $this->view->expects($this->once())
- ->method('getFileInfo')
- ->with('FolderA/test.txt')
- ->willReturn($fileInfoFolderATestTXT);
-
- $this->plugin->checkMove('FolderA/test.txt', 'test.txt');
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotFound
- * @expectedExceptionMessage FolderA/test.txt does not exist
- */
- public function testMoveSrcNotExist() {
- $this->view->expects($this->once())
- ->method('getFileInfo')
- ->with('FolderA/test.txt')
- ->willReturn(false);
-
- $this->plugin->checkMove('FolderA/test.txt', 'test.txt');
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/filesreportplugin.php b/apps/dav/tests/unit/connector/sabre/filesreportplugin.php
deleted file mode 100644
index 87973ef0071..00000000000
--- a/apps/dav/tests/unit/connector/sabre/filesreportplugin.php
+++ /dev/null
@@ -1,603 +0,0 @@
-<?php
-/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OCA\DAV\Connector\Sabre\FilesReportPlugin as FilesReportPluginImplementation;
-use Sabre\DAV\Exception\NotFound;
-use OCP\SystemTag\ISystemTagObjectMapper;
-use OC\Files\View;
-use OCP\Files\Folder;
-use OCP\IGroupManager;
-use OCP\SystemTag\ISystemTagManager;
-
-class FilesReportPlugin extends \Test\TestCase {
- /** @var \Sabre\DAV\Server|\PHPUnit_Framework_MockObject_MockObject */
- private $server;
-
- /** @var \Sabre\DAV\Tree|\PHPUnit_Framework_MockObject_MockObject */
- private $tree;
-
- /** @var ISystemTagObjectMapper|\PHPUnit_Framework_MockObject_MockObject */
- private $tagMapper;
-
- /** @var ISystemTagManager|\PHPUnit_Framework_MockObject_MockObject */
- private $tagManager;
-
- /** @var \OCP\IUserSession */
- private $userSession;
-
- /** @var FilesReportPluginImplementation */
- private $plugin;
-
- /** @var View|\PHPUnit_Framework_MockObject_MockObject **/
- private $view;
-
- /** @var IGroupManager|\PHPUnit_Framework_MockObject_MockObject **/
- private $groupManager;
-
- /** @var Folder|\PHPUnit_Framework_MockObject_MockObject **/
- private $userFolder;
-
- public function setUp() {
- parent::setUp();
- $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->view = $this->getMockBuilder('\OC\Files\View')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
- ->setConstructorArgs([$this->tree])
- ->setMethods(['getRequestUri'])
- ->getMock();
-
- $this->groupManager = $this->getMockBuilder('\OCP\IGroupManager')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->userFolder = $this->getMockBuilder('\OCP\Files\Folder')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager');
- $this->tagMapper = $this->getMock('\OCP\SystemTag\ISystemTagObjectMapper');
- $this->userSession = $this->getMock('\OCP\IUserSession');
-
- $user = $this->getMock('\OCP\IUser');
- $user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('testuser'));
- $this->userSession->expects($this->any())
- ->method('getUser')
- ->will($this->returnValue($user));
-
- $this->plugin = new FilesReportPluginImplementation(
- $this->tree,
- $this->view,
- $this->tagManager,
- $this->tagMapper,
- $this->userSession,
- $this->groupManager,
- $this->userFolder
- );
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\ReportNotSupported
- */
- public function testOnReportInvalidNode() {
- $path = 'totally/unrelated/13';
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/' . $path)
- ->will($this->returnValue($this->getMock('\Sabre\DAV\INode')));
-
- $this->server->expects($this->any())
- ->method('getRequestUri')
- ->will($this->returnValue($path));
- $this->plugin->initialize($this->server);
-
- $this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, [], '/' . $path);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\ReportNotSupported
- */
- public function testOnReportInvalidReportName() {
- $path = 'test';
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/' . $path)
- ->will($this->returnValue($this->getMock('\Sabre\DAV\INode')));
-
- $this->server->expects($this->any())
- ->method('getRequestUri')
- ->will($this->returnValue($path));
- $this->plugin->initialize($this->server);
-
- $this->plugin->onReport('{whoever}whatever', [], '/' . $path);
- }
-
- public function testOnReport() {
- $path = 'test';
-
- $parameters = [
- [
- 'name' => '{DAV:}prop',
- 'value' => [
- ['name' => '{DAV:}getcontentlength', 'value' => ''],
- ['name' => '{http://owncloud.org/ns}size', 'value' => ''],
- ],
- ],
- [
- 'name' => '{http://owncloud.org/ns}filter-rules',
- 'value' => [
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
- ],
- ],
- ];
-
- $this->groupManager->expects($this->any())
- ->method('isAdmin')
- ->will($this->returnValue(true));
-
- $this->tagMapper->expects($this->at(0))
- ->method('getObjectIdsForTags')
- ->with('123', 'files')
- ->will($this->returnValue(['111', '222']));
- $this->tagMapper->expects($this->at(1))
- ->method('getObjectIdsForTags')
- ->with('456', 'files')
- ->will($this->returnValue(['111', '222', '333']));
-
- $reportTargetNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
- ->disableOriginalConstructor()
- ->getMock();
-
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
- $response->expects($this->once())
- ->method('setHeader')
- ->with('Content-Type', 'application/xml; charset=utf-8');
-
- $response->expects($this->once())
- ->method('setStatus')
- ->with(207);
-
- $response->expects($this->once())
- ->method('setBody');
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/' . $path)
- ->will($this->returnValue($reportTargetNode));
-
- $filesNode1 = $this->getMockBuilder('\OCP\Files\Folder')
- ->disableOriginalConstructor()
- ->getMock();
- $filesNode2 = $this->getMockBuilder('\OCP\Files\File')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->userFolder->expects($this->at(0))
- ->method('getById')
- ->with('111')
- ->will($this->returnValue([$filesNode1]));
- $this->userFolder->expects($this->at(1))
- ->method('getById')
- ->with('222')
- ->will($this->returnValue([$filesNode2]));
-
- $this->server->expects($this->any())
- ->method('getRequestUri')
- ->will($this->returnValue($path));
- $this->server->httpResponse = $response;
- $this->plugin->initialize($this->server);
-
- $this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, $parameters, '/' . $path);
- }
-
- public function testFindNodesByFileIdsRoot() {
- $filesNode1 = $this->getMockBuilder('\OCP\Files\Folder')
- ->disableOriginalConstructor()
- ->getMock();
- $filesNode1->expects($this->once())
- ->method('getName')
- ->will($this->returnValue('first node'));
-
- $filesNode2 = $this->getMockBuilder('\OCP\Files\File')
- ->disableOriginalConstructor()
- ->getMock();
- $filesNode2->expects($this->once())
- ->method('getName')
- ->will($this->returnValue('second node'));
-
- $reportTargetNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
- ->disableOriginalConstructor()
- ->getMock();
- $reportTargetNode->expects($this->any())
- ->method('getPath')
- ->will($this->returnValue('/'));
-
- $this->userFolder->expects($this->at(0))
- ->method('getById')
- ->with('111')
- ->will($this->returnValue([$filesNode1]));
- $this->userFolder->expects($this->at(1))
- ->method('getById')
- ->with('222')
- ->will($this->returnValue([$filesNode2]));
-
- /** @var \OCA\DAV\Connector\Sabre\Directory|\PHPUnit_Framework_MockObject_MockObject $reportTargetNode */
- $result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']);
-
- $this->assertCount(2, $result);
- $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\Directory', $result[0]);
- $this->assertEquals('first node', $result[0]->getName());
- $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\File', $result[1]);
- $this->assertEquals('second node', $result[1]->getName());
- }
-
- public function testFindNodesByFileIdsSubDir() {
- $filesNode1 = $this->getMockBuilder('\OCP\Files\Folder')
- ->disableOriginalConstructor()
- ->getMock();
- $filesNode1->expects($this->once())
- ->method('getName')
- ->will($this->returnValue('first node'));
-
- $filesNode2 = $this->getMockBuilder('\OCP\Files\File')
- ->disableOriginalConstructor()
- ->getMock();
- $filesNode2->expects($this->once())
- ->method('getName')
- ->will($this->returnValue('second node'));
-
- $reportTargetNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
- ->disableOriginalConstructor()
- ->getMock();
- $reportTargetNode->expects($this->any())
- ->method('getPath')
- ->will($this->returnValue('/sub1/sub2'));
-
-
- $subNode = $this->getMockBuilder('\OCP\Files\Folder')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->userFolder->expects($this->at(0))
- ->method('get')
- ->with('/sub1/sub2')
- ->will($this->returnValue($subNode));
-
- $subNode->expects($this->at(0))
- ->method('getById')
- ->with('111')
- ->will($this->returnValue([$filesNode1]));
- $subNode->expects($this->at(1))
- ->method('getById')
- ->with('222')
- ->will($this->returnValue([$filesNode2]));
-
- /** @var \OCA\DAV\Connector\Sabre\Directory|\PHPUnit_Framework_MockObject_MockObject $reportTargetNode */
- $result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']);
-
- $this->assertCount(2, $result);
- $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\Directory', $result[0]);
- $this->assertEquals('first node', $result[0]->getName());
- $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\File', $result[1]);
- $this->assertEquals('second node', $result[1]->getName());
- }
-
- public function testPrepareResponses() {
- $requestedProps = ['{DAV:}getcontentlength', '{http://owncloud.org/ns}fileid', '{DAV:}resourcetype'];
-
- $node1 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
- ->disableOriginalConstructor()
- ->getMock();
- $node2 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
- ->disableOriginalConstructor()
- ->getMock();
-
- $node1->expects($this->once())
- ->method('getInternalFileId')
- ->will($this->returnValue('111'));
- $node2->expects($this->once())
- ->method('getInternalFileId')
- ->will($this->returnValue('222'));
- $node2->expects($this->once())
- ->method('getSize')
- ->will($this->returnValue(1024));
-
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesPlugin($this->tree, $this->view));
- $this->plugin->initialize($this->server);
- $responses = $this->plugin->prepareResponses($requestedProps, [$node1, $node2]);
-
- $this->assertCount(2, $responses);
-
- $this->assertEquals(200, $responses[0]->getHttpStatus());
- $this->assertEquals(200, $responses[1]->getHttpStatus());
-
- $props1 = $responses[0]->getResponseProperties();
- $this->assertEquals('111', $props1[200]['{http://owncloud.org/ns}fileid']);
- $this->assertNull($props1[404]['{DAV:}getcontentlength']);
- $this->assertInstanceOf('\Sabre\DAV\Xml\Property\ResourceType', $props1[200]['{DAV:}resourcetype']);
- $resourceType1 = $props1[200]['{DAV:}resourcetype']->getValue();
- $this->assertEquals('{DAV:}collection', $resourceType1[0]);
-
- $props2 = $responses[1]->getResponseProperties();
- $this->assertEquals('1024', $props2[200]['{DAV:}getcontentlength']);
- $this->assertEquals('222', $props2[200]['{http://owncloud.org/ns}fileid']);
- $this->assertInstanceOf('\Sabre\DAV\Xml\Property\ResourceType', $props2[200]['{DAV:}resourcetype']);
- $this->assertCount(0, $props2[200]['{DAV:}resourcetype']->getValue());
- }
-
- public function testProcessFilterRulesSingle() {
- $this->groupManager->expects($this->any())
- ->method('isAdmin')
- ->will($this->returnValue(true));
-
- $this->tagMapper->expects($this->exactly(1))
- ->method('getObjectIdsForTags')
- ->withConsecutive(
- ['123', 'files']
- )
- ->willReturnMap([
- ['123', 'files', 0, '', ['111', '222']],
- ]);
-
- $rules = [
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
- ];
-
- $this->assertEquals(['111', '222'], $this->invokePrivate($this->plugin, 'processFilterRules', [$rules]));
- }
-
- public function testProcessFilterRulesAndCondition() {
- $this->groupManager->expects($this->any())
- ->method('isAdmin')
- ->will($this->returnValue(true));
-
- $this->tagMapper->expects($this->exactly(2))
- ->method('getObjectIdsForTags')
- ->withConsecutive(
- ['123', 'files'],
- ['456', 'files']
- )
- ->willReturnMap([
- ['123', 'files', 0, '', ['111', '222']],
- ['456', 'files', 0, '', ['222', '333']],
- ]);
-
- $rules = [
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
- ];
-
- $this->assertEquals(['222'], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
- }
-
- public function testProcessFilterRulesAndConditionWithOneEmptyResult() {
- $this->groupManager->expects($this->any())
- ->method('isAdmin')
- ->will($this->returnValue(true));
-
- $this->tagMapper->expects($this->exactly(2))
- ->method('getObjectIdsForTags')
- ->withConsecutive(
- ['123', 'files'],
- ['456', 'files']
- )
- ->willReturnMap([
- ['123', 'files', 0, '', ['111', '222']],
- ['456', 'files', 0, '', []],
- ]);
-
- $rules = [
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
- ];
-
- $this->assertEquals([], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
- }
-
- public function testProcessFilterRulesAndConditionWithFirstEmptyResult() {
- $this->groupManager->expects($this->any())
- ->method('isAdmin')
- ->will($this->returnValue(true));
-
- $this->tagMapper->expects($this->exactly(1))
- ->method('getObjectIdsForTags')
- ->withConsecutive(
- ['123', 'files'],
- ['456', 'files']
- )
- ->willReturnMap([
- ['123', 'files', 0, '', []],
- ['456', 'files', 0, '', ['111', '222']],
- ]);
-
- $rules = [
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
- ];
-
- $this->assertEquals([], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
- }
-
- public function testProcessFilterRulesAndConditionWithEmptyMidResult() {
- $this->groupManager->expects($this->any())
- ->method('isAdmin')
- ->will($this->returnValue(true));
-
- $this->tagMapper->expects($this->exactly(2))
- ->method('getObjectIdsForTags')
- ->withConsecutive(
- ['123', 'files'],
- ['456', 'files'],
- ['789', 'files']
- )
- ->willReturnMap([
- ['123', 'files', 0, '', ['111', '222']],
- ['456', 'files', 0, '', ['333']],
- ['789', 'files', 0, '', ['111', '222']],
- ]);
-
- $rules = [
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '789'],
- ];
-
- $this->assertEquals([], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
- }
-
- public function testProcessFilterRulesInvisibleTagAsAdmin() {
- $this->groupManager->expects($this->any())
- ->method('isAdmin')
- ->will($this->returnValue(true));
-
- $tag1 = $this->getMock('\OCP\SystemTag\ISystemTag');
- $tag1->expects($this->any())
- ->method('getId')
- ->will($this->returnValue('123'));
- $tag1->expects($this->any())
- ->method('isUserVisible')
- ->will($this->returnValue(true));
-
- $tag2 = $this->getMock('\OCP\SystemTag\ISystemTag');
- $tag2->expects($this->any())
- ->method('getId')
- ->will($this->returnValue('123'));
- $tag2->expects($this->any())
- ->method('isUserVisible')
- ->will($this->returnValue(false));
-
- // no need to fetch tags to check permissions
- $this->tagManager->expects($this->never())
- ->method('getTagsByIds');
-
- $this->tagMapper->expects($this->at(0))
- ->method('getObjectIdsForTags')
- ->with('123')
- ->will($this->returnValue(['111', '222']));
- $this->tagMapper->expects($this->at(1))
- ->method('getObjectIdsForTags')
- ->with('456')
- ->will($this->returnValue(['222', '333']));
-
- $rules = [
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
- ];
-
- $this->assertEquals(['222'], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
- }
-
- /**
- * @expectedException \OCP\SystemTag\TagNotFoundException
- */
- public function testProcessFilterRulesInvisibleTagAsUser() {
- $this->groupManager->expects($this->any())
- ->method('isAdmin')
- ->will($this->returnValue(false));
-
- $tag1 = $this->getMock('\OCP\SystemTag\ISystemTag');
- $tag1->expects($this->any())
- ->method('getId')
- ->will($this->returnValue('123'));
- $tag1->expects($this->any())
- ->method('isUserVisible')
- ->will($this->returnValue(true));
-
- $tag2 = $this->getMock('\OCP\SystemTag\ISystemTag');
- $tag2->expects($this->any())
- ->method('getId')
- ->will($this->returnValue('123'));
- $tag2->expects($this->any())
- ->method('isUserVisible')
- ->will($this->returnValue(false)); // invisible
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['123', '456'])
- ->will($this->returnValue([$tag1, $tag2]));
-
- $rules = [
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
- ];
-
- $this->invokePrivate($this->plugin, 'processFilterRules', [$rules]);
- }
-
- public function testProcessFilterRulesVisibleTagAsUser() {
- $this->groupManager->expects($this->any())
- ->method('isAdmin')
- ->will($this->returnValue(false));
-
- $tag1 = $this->getMock('\OCP\SystemTag\ISystemTag');
- $tag1->expects($this->any())
- ->method('getId')
- ->will($this->returnValue('123'));
- $tag1->expects($this->any())
- ->method('isUserVisible')
- ->will($this->returnValue(true));
-
- $tag2 = $this->getMock('\OCP\SystemTag\ISystemTag');
- $tag2->expects($this->any())
- ->method('getId')
- ->will($this->returnValue('123'));
- $tag2->expects($this->any())
- ->method('isUserVisible')
- ->will($this->returnValue(true));
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['123', '456'])
- ->will($this->returnValue([$tag1, $tag2]));
-
- $this->tagMapper->expects($this->at(0))
- ->method('getObjectIdsForTags')
- ->with('123')
- ->will($this->returnValue(['111', '222']));
- $this->tagMapper->expects($this->at(1))
- ->method('getObjectIdsForTags')
- ->with('456')
- ->will($this->returnValue(['222', '333']));
-
- $rules = [
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
- ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
- ];
-
- $this->assertEquals(['222'], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/node.php b/apps/dav/tests/unit/connector/sabre/node.php
deleted file mode 100644
index cde8e746dc3..00000000000
--- a/apps/dav/tests/unit/connector/sabre/node.php
+++ /dev/null
@@ -1,130 +0,0 @@
-<?php
-/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-class Node extends \Test\TestCase {
- public function davPermissionsProvider() {
- return array(
- array(\OCP\Constants::PERMISSION_ALL, 'file', false, false, 'RDNVW'),
- array(\OCP\Constants::PERMISSION_ALL, 'dir', false, false, 'RDNVCK'),
- array(\OCP\Constants::PERMISSION_ALL, 'file', true, false, 'SRDNVW'),
- array(\OCP\Constants::PERMISSION_ALL, 'file', true, true, 'SRMDNVW'),
- array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_SHARE, 'file', true, false, 'SDNVW'),
- array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_UPDATE, 'file', false, false, 'RD'),
- array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_DELETE, 'file', false, false, 'RNVW'),
- array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE, 'file', false, false, 'RDNVW'),
- array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE, 'dir', false, false, 'RDNV'),
- );
- }
-
- /**
- * @dataProvider davPermissionsProvider
- */
- public function testDavPermissions($permissions, $type, $shared, $mounted, $expected) {
- $info = $this->getMockBuilder('\OC\Files\FileInfo')
- ->disableOriginalConstructor()
- ->setMethods(array('getPermissions', 'isShared', 'isMounted', 'getType'))
- ->getMock();
- $info->expects($this->any())
- ->method('getPermissions')
- ->will($this->returnValue($permissions));
- $info->expects($this->any())
- ->method('isShared')
- ->will($this->returnValue($shared));
- $info->expects($this->any())
- ->method('isMounted')
- ->will($this->returnValue($mounted));
- $info->expects($this->any())
- ->method('getType')
- ->will($this->returnValue($type));
- $view = $this->getMock('\OC\Files\View');
-
- $node = new \OCA\DAV\Connector\Sabre\File($view, $info);
- $this->assertEquals($expected, $node->getDavPermissions());
- }
-
- public function sharePermissionsProvider() {
- return [
- [\OCP\Files\FileInfo::TYPE_FILE, 1, 0],
- [\OCP\Files\FileInfo::TYPE_FILE, 3, 0],
- [\OCP\Files\FileInfo::TYPE_FILE, 5, 0],
- [\OCP\Files\FileInfo::TYPE_FILE, 7, 0],
- [\OCP\Files\FileInfo::TYPE_FILE, 9, 0],
- [\OCP\Files\FileInfo::TYPE_FILE, 11, 0],
- [\OCP\Files\FileInfo::TYPE_FILE, 13, 0],
- [\OCP\Files\FileInfo::TYPE_FILE, 15, 0],
- [\OCP\Files\FileInfo::TYPE_FILE, 17, 17],
- [\OCP\Files\FileInfo::TYPE_FILE, 19, 19],
- [\OCP\Files\FileInfo::TYPE_FILE, 21, 17],
- [\OCP\Files\FileInfo::TYPE_FILE, 23, 19],
- [\OCP\Files\FileInfo::TYPE_FILE, 25, 17],
- [\OCP\Files\FileInfo::TYPE_FILE, 27, 19],
- [\OCP\Files\FileInfo::TYPE_FILE, 29, 17],
- [\OCP\Files\FileInfo::TYPE_FILE, 30, 0],
- [\OCP\Files\FileInfo::TYPE_FILE, 31, 19],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 1, 0],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 3, 0],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 5, 0],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 7, 0],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 9, 0],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 11, 0],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 13, 0],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 15, 0],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 17, 17],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 19, 19],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 21, 21],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 23, 23],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 25, 25],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 27, 27],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 29, 29],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 30, 0],
- [\OCP\Files\FileInfo::TYPE_FOLDER, 31, 31],
- ];
- }
-
- /**
- * @dataProvider sharePermissionsProvider
- */
- public function testSharePermissions($type, $permissions, $expected) {
- $storage = $this->getMock('\OCP\Files\Storage');
- $storage->method('getPermissions')->willReturn($permissions);
-
- $mountpoint = $this->getMock('\OCP\Files\Mount\IMountPoint');
- $mountpoint->method('getMountPoint')->willReturn('myPath');
-
- $info = $this->getMockBuilder('\OC\Files\FileInfo')
- ->disableOriginalConstructor()
- ->setMethods(['getStorage', 'getType', 'getMountPoint'])
- ->getMock();
-
- $info->method('getStorage')->willReturn($storage);
- $info->method('getType')->willReturn($type);
- $info->method('getMountPoint')->willReturn($mountpoint);
-
- $view = $this->getMock('\OC\Files\View');
-
- $node = new \OCA\DAV\Connector\Sabre\File($view, $info);
- $this->assertEquals($expected, $node->getSharePermissions());
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/objecttree.php b/apps/dav/tests/unit/connector/sabre/objecttree.php
deleted file mode 100644
index e5e858ef17b..00000000000
--- a/apps/dav/tests/unit/connector/sabre/objecttree.php
+++ /dev/null
@@ -1,355 +0,0 @@
-<?php
-/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-
-use OC\Files\FileInfo;
-use OC\Files\Storage\Temporary;
-
-class TestDoubleFileView extends \OC\Files\View {
-
- public function __construct($updatables, $deletables, $canRename = true) {
- $this->updatables = $updatables;
- $this->deletables = $deletables;
- $this->canRename = $canRename;
- }
-
- public function isUpdatable($path) {
- return $this->updatables[$path];
- }
-
- public function isCreatable($path) {
- return $this->updatables[$path];
- }
-
- public function isDeletable($path) {
- return $this->deletables[$path];
- }
-
- public function rename($path1, $path2) {
- return $this->canRename;
- }
-
- public function getRelativePath($path) {
- return $path;
- }
-}
-
-/**
- * Class ObjectTree
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\Unit\Connector\Sabre
- */
-class ObjectTree extends \Test\TestCase {
-
- /**
- * @dataProvider moveFailedProvider
- * @expectedException \Sabre\DAV\Exception\Forbidden
- */
- public function testMoveFailed($source, $destination, $updatables, $deletables) {
- $this->moveTest($source, $destination, $updatables, $deletables);
- }
-
- /**
- * @dataProvider moveSuccessProvider
- */
- public function testMoveSuccess($source, $destination, $updatables, $deletables) {
- $this->moveTest($source, $destination, $updatables, $deletables);
- $this->assertTrue(true);
- }
-
- /**
- * @dataProvider moveFailedInvalidCharsProvider
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath
- */
- public function testMoveFailedInvalidChars($source, $destination, $updatables, $deletables) {
- $this->moveTest($source, $destination, $updatables, $deletables);
- }
-
- function moveFailedInvalidCharsProvider() {
- return array(
- array('a/b', 'a/*', array('a' => true, 'a/b' => true, 'a/c*' => false), array()),
- );
- }
-
- function moveFailedProvider() {
- return array(
- array('a/b', 'a/c', array('a' => false, 'a/b' => false, 'a/c' => false), array()),
- array('a/b', 'b/b', array('a' => false, 'a/b' => false, 'b' => false, 'b/b' => false), array()),
- array('a/b', 'b/b', array('a' => false, 'a/b' => true, 'b' => false, 'b/b' => false), array()),
- array('a/b', 'b/b', array('a' => true, 'a/b' => true, 'b' => false, 'b/b' => false), array()),
- array('a/b', 'b/b', array('a' => true, 'a/b' => true, 'b' => true, 'b/b' => false), array('a/b' => false)),
- array('a/b', 'a/c', array('a' => false, 'a/b' => true, 'a/c' => false), array()),
- );
- }
-
- function moveSuccessProvider() {
- return array(
- array('a/b', 'b/b', array('a' => true, 'a/b' => true, 'b' => true, 'b/b' => false), array('a/b' => true)),
- // older files with special chars can still be renamed to valid names
- array('a/b*', 'b/b', array('a' => true, 'a/b*' => true, 'b' => true, 'b/b' => false), array('a/b*' => true)),
- );
- }
-
- /**
- * @param $source
- * @param $destination
- * @param $updatables
- */
- private function moveTest($source, $destination, $updatables, $deletables) {
- $view = new TestDoubleFileView($updatables, $deletables);
-
- $info = new FileInfo('', null, null, array(), null);
-
- $rootDir = new \OCA\DAV\Connector\Sabre\Directory($view, $info);
- $objectTree = $this->getMock('\OCA\DAV\Connector\Sabre\ObjectTree',
- array('nodeExists', 'getNodeForPath'),
- array($rootDir, $view));
-
- $objectTree->expects($this->once())
- ->method('getNodeForPath')
- ->with($this->identicalTo($source))
- ->will($this->returnValue(false));
-
- /** @var $objectTree \OCA\DAV\Connector\Sabre\ObjectTree */
- $mountManager = \OC\Files\Filesystem::getMountManager();
- $objectTree->init($rootDir, $view, $mountManager);
- $objectTree->move($source, $destination);
- }
-
- /**
- * @dataProvider nodeForPathProvider
- */
- public function testGetNodeForPath(
- $inputFileName,
- $fileInfoQueryPath,
- $outputFileName,
- $type,
- $enableChunkingHeader
- ) {
-
- if ($enableChunkingHeader) {
- $_SERVER['HTTP_OC_CHUNKED'] = true;
- }
-
- $rootNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
- ->disableOriginalConstructor()
- ->getMock();
- $mountManager = $this->getMock('\OC\Files\Mount\Manager');
- $view = $this->getMock('\OC\Files\View');
- $fileInfo = $this->getMock('\OCP\Files\FileInfo');
- $fileInfo->expects($this->once())
- ->method('getType')
- ->will($this->returnValue($type));
- $fileInfo->expects($this->once())
- ->method('getName')
- ->will($this->returnValue($outputFileName));
-
- $view->expects($this->once())
- ->method('getFileInfo')
- ->with($fileInfoQueryPath)
- ->will($this->returnValue($fileInfo));
-
- $tree = new \OCA\DAV\Connector\Sabre\ObjectTree();
- $tree->init($rootNode, $view, $mountManager);
-
- $node = $tree->getNodeForPath($inputFileName);
-
- $this->assertNotNull($node);
- $this->assertEquals($outputFileName, $node->getName());
-
- if ($type === 'file') {
- $this->assertTrue($node instanceof \OCA\DAV\Connector\Sabre\File);
- } else {
- $this->assertTrue($node instanceof \OCA\DAV\Connector\Sabre\Directory);
- }
-
- unset($_SERVER['HTTP_OC_CHUNKED']);
- }
-
- function nodeForPathProvider() {
- return array(
- // regular file
- array(
- 'regularfile.txt',
- 'regularfile.txt',
- 'regularfile.txt',
- 'file',
- false
- ),
- // regular directory
- array(
- 'regulardir',
- 'regulardir',
- 'regulardir',
- 'dir',
- false
- ),
- // regular file with chunking
- array(
- 'regularfile.txt',
- 'regularfile.txt',
- 'regularfile.txt',
- 'file',
- true
- ),
- // regular directory with chunking
- array(
- 'regulardir',
- 'regulardir',
- 'regulardir',
- 'dir',
- true
- ),
- // file with chunky file name
- array(
- 'regularfile.txt-chunking-123566789-10-1',
- 'regularfile.txt',
- 'regularfile.txt',
- 'file',
- true
- ),
- // regular file in subdir
- array(
- 'subdir/regularfile.txt',
- 'subdir/regularfile.txt',
- 'regularfile.txt',
- 'file',
- false
- ),
- // regular directory in subdir
- array(
- 'subdir/regulardir',
- 'subdir/regulardir',
- 'regulardir',
- 'dir',
- false
- ),
- // file with chunky file name in subdir
- array(
- 'subdir/regularfile.txt-chunking-123566789-10-1',
- 'subdir/regularfile.txt',
- 'regularfile.txt',
- 'file',
- true
- ),
- );
- }
-
- /**
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath
- */
- public function testGetNodeForPathInvalidPath() {
- $path = '/foo\bar';
-
-
- $storage = new Temporary([]);
-
- $view = $this->getMock('\OC\Files\View', ['resolvePath']);
- $view->expects($this->once())
- ->method('resolvePath')
- ->will($this->returnCallback(function($path) use ($storage){
- return [$storage, ltrim($path, '/')];
- }));
-
- $rootNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
- ->disableOriginalConstructor()
- ->getMock();
- $mountManager = $this->getMock('\OC\Files\Mount\Manager');
-
- $tree = new \OCA\DAV\Connector\Sabre\ObjectTree();
- $tree->init($rootNode, $view, $mountManager);
-
- $tree->getNodeForPath($path);
- }
-
- public function testGetNodeForPathRoot() {
- $path = '/';
-
-
- $storage = new Temporary([]);
-
- $view = $this->getMock('\OC\Files\View', ['resolvePath']);
- $view->expects($this->any())
- ->method('resolvePath')
- ->will($this->returnCallback(function ($path) use ($storage) {
- return [$storage, ltrim($path, '/')];
- }));
-
- $rootNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
- ->disableOriginalConstructor()
- ->getMock();
- $mountManager = $this->getMock('\OC\Files\Mount\Manager');
-
- $tree = new \OCA\DAV\Connector\Sabre\ObjectTree();
- $tree->init($rootNode, $view, $mountManager);
-
- $this->assertInstanceOf('\Sabre\DAV\INode', $tree->getNodeForPath($path));
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\Forbidden
- * @expectedExceptionMessage Could not copy directory nameOfSourceNode, target exists
- */
- public function testFailingMove() {
- $source = 'a/b';
- $destination = 'b/b';
- $updatables = array('a' => true, 'a/b' => true, 'b' => true, 'b/b' => false);
- $deletables = array('a/b' => true);
-
- $view = new TestDoubleFileView($updatables, $deletables);
-
- $info = new FileInfo('', null, null, array(), null);
-
- $rootDir = new \OCA\DAV\Connector\Sabre\Directory($view, $info);
- $objectTree = $this->getMock('\OCA\DAV\Connector\Sabre\ObjectTree',
- array('nodeExists', 'getNodeForPath'),
- array($rootDir, $view));
-
- $sourceNode = $this->getMockBuilder('\Sabre\DAV\ICollection')
- ->disableOriginalConstructor()
- ->getMock();
- $sourceNode->expects($this->once())
- ->method('getName')
- ->will($this->returnValue('nameOfSourceNode'));
-
- $objectTree->expects($this->once())
- ->method('nodeExists')
- ->with($this->identicalTo($destination))
- ->will($this->returnValue(true));
- $objectTree->expects($this->once())
- ->method('getNodeForPath')
- ->with($this->identicalTo($source))
- ->will($this->returnValue($sourceNode));
-
- /** @var $objectTree \OCA\DAV\Connector\Sabre\ObjectTree */
- $mountManager = \OC\Files\Filesystem::getMountManager();
- $objectTree->init($rootDir, $view, $mountManager);
- $objectTree->move($source, $destination);
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/principal.php b/apps/dav/tests/unit/connector/sabre/principal.php
deleted file mode 100644
index 1747885240a..00000000000
--- a/apps/dav/tests/unit/connector/sabre/principal.php
+++ /dev/null
@@ -1,258 +0,0 @@
-<?php
-/**
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-use OCP\IGroupManager;
-use \Sabre\DAV\PropPatch;
-use OCP\IUserManager;
-use Test\TestCase;
-
-class Principal extends TestCase {
- /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject */
- private $userManager;
- /** @var \OCA\DAV\Connector\Sabre\Principal */
- private $connector;
- /** @var IGroupManager | \PHPUnit_Framework_MockObject_MockObject */
- private $groupManager;
-
- public function setUp() {
- $this->userManager = $this->getMockBuilder('\OCP\IUserManager')
- ->disableOriginalConstructor()->getMock();
- $this->groupManager = $this->getMockBuilder('\OCP\IGroupManager')
- ->disableOriginalConstructor()->getMock();
-
- $this->connector = new \OCA\DAV\Connector\Sabre\Principal(
- $this->userManager,
- $this->groupManager);
- parent::setUp();
- }
-
- public function testGetPrincipalsByPrefixWithoutPrefix() {
- $response = $this->connector->getPrincipalsByPrefix('');
- $this->assertSame([], $response);
- }
-
- public function testGetPrincipalsByPrefixWithUsers() {
- $fooUser = $this->getMockBuilder('\OC\User\User')
- ->disableOriginalConstructor()->getMock();
- $fooUser
- ->expects($this->exactly(1))
- ->method('getUID')
- ->will($this->returnValue('foo'));
- $fooUser
- ->expects($this->exactly(1))
- ->method('getDisplayName')
- ->will($this->returnValue('Dr. Foo-Bar'));
- $fooUser
- ->expects($this->exactly(1))
- ->method('getEMailAddress')
- ->will($this->returnValue(''));
- $barUser = $this->getMockBuilder('\OC\User\User')
- ->disableOriginalConstructor()->getMock();
- $barUser
- ->expects($this->exactly(1))
- ->method('getUID')
- ->will($this->returnValue('bar'));
- $barUser
- ->expects($this->exactly(1))
- ->method('getEMailAddress')
- ->will($this->returnValue('bar@owncloud.org'));
- $this->userManager
- ->expects($this->once())
- ->method('search')
- ->with('')
- ->will($this->returnValue([$fooUser, $barUser]));
-
- $expectedResponse = [
- 0 => [
- 'uri' => 'principals/users/foo',
- '{DAV:}displayname' => 'Dr. Foo-Bar'
- ],
- 1 => [
- 'uri' => 'principals/users/bar',
- '{DAV:}displayname' => 'bar',
- '{http://sabredav.org/ns}email-address' => 'bar@owncloud.org'
- ]
- ];
- $response = $this->connector->getPrincipalsByPrefix('principals/users');
- $this->assertSame($expectedResponse, $response);
- }
-
- public function testGetPrincipalsByPrefixEmpty() {
- $this->userManager
- ->expects($this->once())
- ->method('search')
- ->with('')
- ->will($this->returnValue([]));
-
- $response = $this->connector->getPrincipalsByPrefix('principals/users');
- $this->assertSame([], $response);
- }
-
- public function testGetPrincipalsByPathWithoutMail() {
- $fooUser = $this->getMockBuilder('\OC\User\User')
- ->disableOriginalConstructor()->getMock();
- $fooUser
- ->expects($this->exactly(1))
- ->method('getUID')
- ->will($this->returnValue('foo'));
- $this->userManager
- ->expects($this->once())
- ->method('get')
- ->with('foo')
- ->will($this->returnValue($fooUser));
-
- $expectedResponse = [
- 'uri' => 'principals/users/foo',
- '{DAV:}displayname' => 'foo'
- ];
- $response = $this->connector->getPrincipalByPath('principals/users/foo');
- $this->assertSame($expectedResponse, $response);
- }
-
- public function testGetPrincipalsByPathWithMail() {
- $fooUser = $this->getMockBuilder('\OC\User\User')
- ->disableOriginalConstructor()->getMock();
- $fooUser
- ->expects($this->exactly(1))
- ->method('getEMailAddress')
- ->will($this->returnValue('foo@owncloud.org'));
- $fooUser
- ->expects($this->exactly(1))
- ->method('getUID')
- ->will($this->returnValue('foo'));
- $this->userManager
- ->expects($this->once())
- ->method('get')
- ->with('foo')
- ->will($this->returnValue($fooUser));
-
- $expectedResponse = [
- 'uri' => 'principals/users/foo',
- '{DAV:}displayname' => 'foo',
- '{http://sabredav.org/ns}email-address' => 'foo@owncloud.org'
- ];
- $response = $this->connector->getPrincipalByPath('principals/users/foo');
- $this->assertSame($expectedResponse, $response);
- }
-
- public function testGetPrincipalsByPathEmpty() {
- $this->userManager
- ->expects($this->once())
- ->method('get')
- ->with('foo')
- ->will($this->returnValue(null));
-
- $response = $this->connector->getPrincipalByPath('principals/users/foo');
- $this->assertSame(null, $response);
- }
-
- public function testGetGroupMemberSet() {
- $fooUser = $this->getMockBuilder('\OC\User\User')
- ->disableOriginalConstructor()->getMock();
- $fooUser
- ->expects($this->exactly(1))
- ->method('getUID')
- ->will($this->returnValue('foo'));
- $this->userManager
- ->expects($this->once())
- ->method('get')
- ->with('foo')
- ->will($this->returnValue($fooUser));
-
- $response = $this->connector->getGroupMemberSet('principals/users/foo');
- $this->assertSame(['principals/users/foo'], $response);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception
- * @expectedExceptionMessage Principal not found
- */
- public function testGetGroupMemberSetEmpty() {
- $this->userManager
- ->expects($this->once())
- ->method('get')
- ->with('foo')
- ->will($this->returnValue(null));
-
- $this->connector->getGroupMemberSet('principals/users/foo');
- }
-
- public function testGetGroupMembership() {
- $fooUser = $this->getMockBuilder('\OC\User\User')
- ->disableOriginalConstructor()->getMock();
- $group = $this->getMockBuilder('\OCP\IGroup')
- ->disableOriginalConstructor()->getMock();
- $group->expects($this->once())
- ->method('getGID')
- ->willReturn('group1');
- $this->userManager
- ->expects($this->once())
- ->method('get')
- ->with('foo')
- ->willReturn($fooUser);
- $this->groupManager
- ->expects($this->once())
- ->method('getUserGroups')
- ->willReturn([
- $group
- ]);
-
- $expectedResponse = [
- 'principals/groups/group1'
- ];
- $response = $this->connector->getGroupMembership('principals/users/foo');
- $this->assertSame($expectedResponse, $response);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception
- * @expectedExceptionMessage Principal not found
- */
- public function testGetGroupMembershipEmpty() {
- $this->userManager
- ->expects($this->once())
- ->method('get')
- ->with('foo')
- ->will($this->returnValue(null));
-
- $this->connector->getGroupMembership('principals/users/foo');
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception
- * @expectedExceptionMessage Setting members of the group is not supported yet
- */
- public function testSetGroupMembership() {
- $this->connector->setGroupMemberSet('principals/users/foo', ['foo']);
- }
-
- public function testUpdatePrincipal() {
- $this->assertSame(0, $this->connector->updatePrincipal('foo', new PropPatch(array())));
- }
-
- public function testSearchPrincipals() {
- $this->assertSame([], $this->connector->searchPrincipals('principals/users', []));
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/quotaplugin.php b/apps/dav/tests/unit/connector/sabre/quotaplugin.php
deleted file mode 100644
index b5a8bfef31c..00000000000
--- a/apps/dav/tests/unit/connector/sabre/quotaplugin.php
+++ /dev/null
@@ -1,223 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-/**
- * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
- * This file is licensed under the Affero General Public License version 3 or
- * later.
- * See the COPYING-README file.
- */
-class QuotaPlugin extends \Test\TestCase {
-
- /**
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var \OCA\DAV\Connector\Sabre\QuotaPlugin
- */
- private $plugin;
-
- private function init($quota, $checkedPath = '') {
- $view = $this->buildFileViewMock($quota, $checkedPath);
- $this->server = new \Sabre\DAV\Server();
- $this->plugin = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\QuotaPlugin')
- ->setConstructorArgs([$view])
- ->setMethods(['getFileChunking'])
- ->getMock();
- $this->plugin->initialize($this->server);
- }
-
- /**
- * @dataProvider lengthProvider
- */
- public function testLength($expected, $headers) {
- $this->init(0);
- $this->plugin->expects($this->never())
- ->method('getFileChunking');
- $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
- $length = $this->plugin->getLength();
- $this->assertEquals($expected, $length);
- }
-
- /**
- * @dataProvider quotaOkayProvider
- */
- public function testCheckQuota($quota, $headers) {
- $this->init($quota);
- $this->plugin->expects($this->never())
- ->method('getFileChunking');
-
- $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
- $result = $this->plugin->checkQuota('');
- $this->assertTrue($result);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\InsufficientStorage
- * @dataProvider quotaExceededProvider
- */
- public function testCheckExceededQuota($quota, $headers) {
- $this->init($quota);
- $this->plugin->expects($this->never())
- ->method('getFileChunking');
-
- $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
- $this->plugin->checkQuota('');
- }
-
- /**
- * @dataProvider quotaOkayProvider
- */
- public function testCheckQuotaOnPath($quota, $headers) {
- $this->init($quota, 'sub/test.txt');
- $this->plugin->expects($this->never())
- ->method('getFileChunking');
-
- $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
- $result = $this->plugin->checkQuota('/sub/test.txt');
- $this->assertTrue($result);
- }
-
- public function quotaOkayProvider() {
- return array(
- array(1024, array()),
- array(1024, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
- array(1024, array('CONTENT-LENGTH' => '512')),
- array(1024, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
- // \OCP\Files\FileInfo::SPACE-UNKNOWN = -2
- array(-2, array()),
- array(-2, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
- array(-2, array('CONTENT-LENGTH' => '512')),
- array(-2, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
- );
- }
-
- public function quotaExceededProvider() {
- return array(
- array(1023, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
- array(511, array('CONTENT-LENGTH' => '512')),
- array(2047, array('OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => '1024')),
- );
- }
-
- public function lengthProvider() {
- return array(
- array(null, array()),
- array(1024, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
- array(512, array('CONTENT-LENGTH' => '512')),
- array(2048, array('OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => '1024')),
- array(4096, array('OC-TOTAL-LENGTH' => '2048', 'X-EXPECTED-ENTITY-LENGTH' => '4096')),
- );
- }
-
- public function quotaChunkedOkProvider() {
- return array(
- array(1024, 0, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
- array(1024, 0, array('CONTENT-LENGTH' => '512')),
- array(1024, 0, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
- // with existing chunks (allowed size = total length - chunk total size)
- array(400, 128, array('X-EXPECTED-ENTITY-LENGTH' => '512')),
- array(400, 128, array('CONTENT-LENGTH' => '512')),
- array(400, 128, array('OC-TOTAL-LENGTH' => '512', 'CONTENT-LENGTH' => '500')),
- // \OCP\Files\FileInfo::SPACE-UNKNOWN = -2
- array(-2, 0, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
- array(-2, 0, array('CONTENT-LENGTH' => '512')),
- array(-2, 0, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
- array(-2, 128, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
- array(-2, 128, array('CONTENT-LENGTH' => '512')),
- array(-2, 128, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
- );
- }
-
- /**
- * @dataProvider quotaChunkedOkProvider
- */
- public function testCheckQuotaChunkedOk($quota, $chunkTotalSize, $headers) {
- $this->init($quota, 'sub/test.txt');
-
- $mockChunking = $this->getMockBuilder('\OC_FileChunking')
- ->disableOriginalConstructor()
- ->getMock();
- $mockChunking->expects($this->once())
- ->method('getCurrentSize')
- ->will($this->returnValue($chunkTotalSize));
-
- $this->plugin->expects($this->once())
- ->method('getFileChunking')
- ->will($this->returnValue($mockChunking));
-
- $headers['OC-CHUNKED'] = 1;
- $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
- $result = $this->plugin->checkQuota('/sub/test.txt-chunking-12345-3-1');
- $this->assertTrue($result);
- }
-
- public function quotaChunkedFailProvider() {
- return array(
- array(400, 0, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
- array(400, 0, array('CONTENT-LENGTH' => '512')),
- array(400, 0, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
- // with existing chunks (allowed size = total length - chunk total size)
- array(380, 128, array('X-EXPECTED-ENTITY-LENGTH' => '512')),
- array(380, 128, array('CONTENT-LENGTH' => '512')),
- array(380, 128, array('OC-TOTAL-LENGTH' => '512', 'CONTENT-LENGTH' => '500')),
- );
- }
-
- /**
- * @dataProvider quotaChunkedFailProvider
- * @expectedException \Sabre\DAV\Exception\InsufficientStorage
- */
- public function testCheckQuotaChunkedFail($quota, $chunkTotalSize, $headers) {
- $this->init($quota, 'sub/test.txt');
-
- $mockChunking = $this->getMockBuilder('\OC_FileChunking')
- ->disableOriginalConstructor()
- ->getMock();
- $mockChunking->expects($this->once())
- ->method('getCurrentSize')
- ->will($this->returnValue($chunkTotalSize));
-
- $this->plugin->expects($this->once())
- ->method('getFileChunking')
- ->will($this->returnValue($mockChunking));
-
- $headers['OC-CHUNKED'] = 1;
- $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
- $this->plugin->checkQuota('/sub/test.txt-chunking-12345-3-1');
- }
-
- private function buildFileViewMock($quota, $checkedPath) {
- // mock filesysten
- $view = $this->getMock('\OC\Files\View', array('free_space'), array(), '', false);
- $view->expects($this->any())
- ->method('free_space')
- ->with($this->identicalTo($checkedPath))
- ->will($this->returnValue($quota));
-
- return $view;
- }
-
-}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php
deleted file mode 100644
index 3d047399a1f..00000000000
--- a/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
-
-use OCP\AppFramework\Http;
-use OCP\Lock\ILockingProvider;
-
-/**
- * Class DownloadTest
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest
- */
-class DownloadTest extends RequestTest {
- public function testDownload() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $view->file_put_contents('foo.txt', 'bar');
-
- $response = $this->request($view, $user, 'pass', 'GET', '/foo.txt');
- $this->assertEquals(Http::STATUS_OK, $response->getStatus());
- $this->assertEquals(stream_get_contents($response->getBody()), 'bar');
- }
-
- /**
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked
- */
- public function testDownloadWriteLocked() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $view->file_put_contents('foo.txt', 'bar');
-
- $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE);
-
- $this->request($view, $user, 'pass', 'GET', '/foo.txt', 'asd');
- }
-
- public function testDownloadReadLocked() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $view->file_put_contents('foo.txt', 'bar');
-
- $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED);
-
- $response = $this->request($view, $user, 'pass', 'GET', '/foo.txt', 'asd');
- $this->assertEquals(Http::STATUS_OK, $response->getStatus());
- $this->assertEquals(stream_get_contents($response->getBody()), 'bar');
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php
deleted file mode 100644
index c5c6d0da0c2..00000000000
--- a/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
-
-use OC\Files\View;
-use Test\Traits\EncryptionTrait;
-
-/**
- * Class EncryptionUploadTest
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest
- */
-class EncryptionUploadTest extends UploadTest {
- use EncryptionTrait;
-
- protected function setupUser($name, $password) {
- $this->createUser($name, $password);
- $tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
- $this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]);
- $this->setupForUser($name, $password);
- $this->loginWithEncryption($name);
- return new View('/' . $name . '/files');
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php b/apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php
deleted file mode 100644
index a6a0f9d3b86..00000000000
--- a/apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
-
-use Sabre\DAV\Exception;
-
-class ExceptionPlugin extends \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin {
- /**
- * @var \Exception[]
- */
- protected $exceptions = [];
-
- public function logException(\Exception $ex) {
- $exceptionClass = get_class($ex);
- if (!isset($this->nonFatalExceptions[$exceptionClass])) {
- $this->exceptions[] = $ex;
- }
- }
-
- /**
- * @return \Exception[]
- */
- public function getExceptions() {
- return $this->exceptions;
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/partfileinrootupload.php b/apps/dav/tests/unit/connector/sabre/requesttest/partfileinrootupload.php
deleted file mode 100644
index 52790c5b00b..00000000000
--- a/apps/dav/tests/unit/connector/sabre/requesttest/partfileinrootupload.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
-
-use OC\Files\View;
-use Test\Traits\EncryptionTrait;
-
-/**
- * Class EncryptionUploadTest
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest
- */
-class PartFileInRootUpload extends UploadTest {
- protected function setUp() {
- $config = \OC::$server->getConfig();
- $mockConfig = $this->getMock('\OCP\IConfig');
- $mockConfig->expects($this->any())
- ->method('getSystemValue')
- ->will($this->returnCallback(function ($key, $default) use ($config) {
- if ($key === 'part_file_in_storage') {
- return false;
- } else {
- return $config->getSystemValue($key, $default);
- }
- }));
- $this->overwriteService('AllConfig', $mockConfig);
- parent::setUp();
- }
-
- protected function tearDown() {
- $this->restoreService('AllConfig');
- return parent::tearDown();
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/sapi.php b/apps/dav/tests/unit/connector/sabre/requesttest/sapi.php
deleted file mode 100644
index 6407d9bc28b..00000000000
--- a/apps/dav/tests/unit/connector/sabre/requesttest/sapi.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
-
-use Sabre\HTTP\Request;
-use Sabre\HTTP\Response;
-
-class Sapi {
- /**
- * @var \Sabre\HTTP\Request
- */
- private $request;
-
- /**
- * @var \Sabre\HTTP\Response
- */
- private $response;
-
- /**
- * This static method will create a new Request object, based on the
- * current PHP request.
- *
- * @return \Sabre\HTTP\Request
- */
- public function getRequest() {
- return $this->request;
- }
-
- public function __construct(Request $request) {
- $this->request = $request;
- }
-
- /**
- * @param \Sabre\HTTP\Response $response
- * @return void
- */
- public function sendResponse(Response $response) {
- // we need to copy the body since we close the source stream
- $copyStream = fopen('php://temp', 'r+');
- if (is_string($response->getBody())) {
- fwrite($copyStream, $response->getBody());
- } else if (is_resource($response->getBody())) {
- stream_copy_to_stream($response->getBody(), $copyStream);
- }
- rewind($copyStream);
- $this->response = new Response($response->getStatus(), $response->getHeaders(), $copyStream);
- }
-
- /**
- * @return \Sabre\HTTP\Response
- */
- public function getResponse() {
- return $this->response;
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php
deleted file mode 100644
index ae30268e366..00000000000
--- a/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php
+++ /dev/null
@@ -1,211 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
-
-use OC\Connector\Sabre\Exception\FileLocked;
-use OCP\AppFramework\Http;
-use OCP\Lock\ILockingProvider;
-
-/**
- * Class UploadTest
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest
- */
-class UploadTest extends RequestTest {
- public function testBasicUpload() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $this->assertFalse($view->file_exists('foo.txt'));
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertTrue($view->file_exists('foo.txt'));
- $this->assertEquals('asd', $view->file_get_contents('foo.txt'));
-
- $info = $view->getFileInfo('foo.txt');
- $this->assertInstanceOf('\OC\Files\FileInfo', $info);
- $this->assertEquals(3, $info->getSize());
- }
-
- public function testUploadOverWrite() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $view->file_put_contents('foo.txt', 'foobar');
-
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
-
- $this->assertEquals(Http::STATUS_NO_CONTENT, $response->getStatus());
- $this->assertEquals('asd', $view->file_get_contents('foo.txt'));
-
- $info = $view->getFileInfo('foo.txt');
- $this->assertInstanceOf('\OC\Files\FileInfo', $info);
- $this->assertEquals(3, $info->getSize());
- }
-
- /**
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked
- */
- public function testUploadOverWriteReadLocked() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $view->file_put_contents('foo.txt', 'bar');
-
- $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED);
-
- $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
- }
-
- /**
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked
- */
- public function testUploadOverWriteWriteLocked() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $view->file_put_contents('foo.txt', 'bar');
-
- $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE);
-
- $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
- }
-
- public function testChunkedUpload() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $this->assertFalse($view->file_exists('foo.txt'));
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
-
- $this->assertEquals(201, $response->getStatus());
- $this->assertFalse($view->file_exists('foo.txt'));
-
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertTrue($view->file_exists('foo.txt'));
-
- $this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
-
- $info = $view->getFileInfo('foo.txt');
- $this->assertInstanceOf('\OC\Files\FileInfo', $info);
- $this->assertEquals(6, $info->getSize());
- }
-
- public function testChunkedUploadOverWrite() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $view->file_put_contents('foo.txt', 'bar');
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertEquals('bar', $view->file_get_contents('foo.txt'));
-
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
-
- $this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
-
- $info = $view->getFileInfo('foo.txt');
- $this->assertInstanceOf('\OC\Files\FileInfo', $info);
- $this->assertEquals(6, $info->getSize());
- }
-
- public function testChunkedUploadOutOfOrder() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $this->assertFalse($view->file_exists('foo.txt'));
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertFalse($view->file_exists('foo.txt'));
-
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
-
- $this->assertEquals(201, $response->getStatus());
- $this->assertTrue($view->file_exists('foo.txt'));
-
- $this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
-
- $info = $view->getFileInfo('foo.txt');
- $this->assertInstanceOf('\OC\Files\FileInfo', $info);
- $this->assertEquals(6, $info->getSize());
- }
-
- /**
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked
- */
- public function testChunkedUploadOutOfOrderReadLocked() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $this->assertFalse($view->file_exists('foo.txt'));
-
- $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED);
-
- try {
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
- } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
- $this->fail('Didn\'t expect locked error for the first chunk on read lock');
- return;
- }
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertFalse($view->file_exists('foo.txt'));
-
- // last chunk should trigger the locked error since it tries to assemble
- $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
- }
-
- /**
- * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked
- */
- public function testChunkedUploadOutOfOrderWriteLocked() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $this->assertFalse($view->file_exists('foo.txt'));
-
- $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE);
-
- try {
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
- } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
- $this->fail('Didn\'t expect locked error for the first chunk on write lock'); // maybe forbid this in the future for write locks only?
- return;
- }
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertFalse($view->file_exists('foo.txt'));
-
- // last chunk should trigger the locked error since it tries to assemble
- $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/sharesplugin.php b/apps/dav/tests/unit/connector/sabre/sharesplugin.php
deleted file mode 100644
index 42f1b539916..00000000000
--- a/apps/dav/tests/unit/connector/sabre/sharesplugin.php
+++ /dev/null
@@ -1,259 +0,0 @@
-<?php
-/**
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-class SharesPlugin extends \Test\TestCase {
-
- const SHARETYPES_PROPERTYNAME = \OCA\DAV\Connector\Sabre\SharesPlugin::SHARETYPES_PROPERTYNAME;
-
- /**
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var \Sabre\DAV\Tree
- */
- private $tree;
-
- /**
- * @var \OCP\Share\IManager
- */
- private $shareManager;
-
- /**
- * @var \OCP\Files\Folder
- */
- private $userFolder;
-
- /**
- * @var \OCA\DAV\Connector\Sabre\SharesPlugin
- */
- private $plugin;
-
- public function setUp() {
- parent::setUp();
- $this->server = new \Sabre\DAV\Server();
- $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
- ->disableOriginalConstructor()
- ->getMock();
- $this->shareManager = $this->getMock('\OCP\Share\IManager');
- $user = $this->getMock('\OCP\IUser');
- $user->expects($this->once())
- ->method('getUID')
- ->will($this->returnValue('user1'));
- $userSession = $this->getMock('\OCP\IUserSession');
- $userSession->expects($this->once())
- ->method('getUser')
- ->will($this->returnValue($user));
-
- $this->userFolder = $this->getMock('\OCP\Files\Folder');
-
- $this->plugin = new \OCA\DAV\Connector\Sabre\SharesPlugin(
- $this->tree,
- $userSession,
- $this->userFolder,
- $this->shareManager
- );
- $this->plugin->initialize($this->server);
- }
-
- /**
- * @dataProvider sharesGetPropertiesDataProvider
- */
- public function testGetProperties($shareTypes) {
- $sabreNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node')
- ->disableOriginalConstructor()
- ->getMock();
- $sabreNode->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(123));
- $sabreNode->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue('/subdir'));
-
- // node API nodes
- $node = $this->getMock('\OCP\Files\Folder');
-
- $this->userFolder->expects($this->once())
- ->method('get')
- ->with('/subdir')
- ->will($this->returnValue($node));
-
- $this->shareManager->expects($this->any())
- ->method('getSharesBy')
- ->with(
- $this->equalTo('user1'),
- $this->anything(),
- $this->anything(),
- $this->equalTo(false),
- $this->equalTo(1)
- )
- ->will($this->returnCallback(function($userId, $requestedShareType, $node, $flag, $limit) use ($shareTypes){
- if (in_array($requestedShareType, $shareTypes)) {
- return ['dummyshare'];
- }
- return [];
- }));
-
- $propFind = new \Sabre\DAV\PropFind(
- '/dummyPath',
- [self::SHARETYPES_PROPERTYNAME],
- 0
- );
-
- $this->plugin->handleGetProperties(
- $propFind,
- $sabreNode
- );
-
- $result = $propFind->getResultForMultiStatus();
-
- $this->assertEmpty($result[404]);
- unset($result[404]);
- $this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes());
- }
-
- /**
- * @dataProvider sharesGetPropertiesDataProvider
- */
- public function testPreloadThenGetProperties($shareTypes) {
- $sabreNode1 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
- ->disableOriginalConstructor()
- ->getMock();
- $sabreNode1->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(111));
- $sabreNode1->expects($this->never())
- ->method('getPath');
- $sabreNode2 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
- ->disableOriginalConstructor()
- ->getMock();
- $sabreNode2->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(222));
- $sabreNode2->expects($this->never())
- ->method('getPath');
-
- $sabreNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
- ->disableOriginalConstructor()
- ->getMock();
- $sabreNode->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(123));
- // never, because we use getDirectoryListing from the Node API instead
- $sabreNode->expects($this->never())
- ->method('getChildren');
- $sabreNode->expects($this->any())
- ->method('getPath')
- ->will($this->returnValue('/subdir'));
-
- // node API nodes
- $node = $this->getMock('\OCP\Files\Folder');
- $node->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(123));
- $node1 = $this->getMock('\OCP\Files\File');
- $node1->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(111));
- $node2 = $this->getMock('\OCP\Files\File');
- $node2->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(222));
- $node->expects($this->once())
- ->method('getDirectoryListing')
- ->will($this->returnValue([$node1, $node2]));
-
- $this->userFolder->expects($this->once())
- ->method('get')
- ->with('/subdir')
- ->will($this->returnValue($node));
-
- $this->shareManager->expects($this->any())
- ->method('getSharesBy')
- ->with(
- $this->equalTo('user1'),
- $this->anything(),
- $this->anything(),
- $this->equalTo(false),
- $this->equalTo(1)
- )
- ->will($this->returnCallback(function($userId, $requestedShareType, $node, $flag, $limit) use ($shareTypes){
- if ($node->getId() === 111 && in_array($requestedShareType, $shareTypes)) {
- return ['dummyshare'];
- }
-
- return [];
- }));
-
- // simulate sabre recursive PROPFIND traversal
- $propFindRoot = new \Sabre\DAV\PropFind(
- '/subdir',
- [self::SHARETYPES_PROPERTYNAME],
- 1
- );
- $propFind1 = new \Sabre\DAV\PropFind(
- '/subdir/test.txt',
- [self::SHARETYPES_PROPERTYNAME],
- 0
- );
- $propFind2 = new \Sabre\DAV\PropFind(
- '/subdir/test2.txt',
- [self::SHARETYPES_PROPERTYNAME],
- 0
- );
-
- $this->plugin->handleGetProperties(
- $propFindRoot,
- $sabreNode
- );
- $this->plugin->handleGetProperties(
- $propFind1,
- $sabreNode1
- );
- $this->plugin->handleGetProperties(
- $propFind2,
- $sabreNode2
- );
-
- $result = $propFind1->getResultForMultiStatus();
-
- $this->assertEmpty($result[404]);
- unset($result[404]);
- $this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes());
- }
-
- function sharesGetPropertiesDataProvider() {
- return [
- [[]],
- [[\OCP\Share::SHARE_TYPE_USER]],
- [[\OCP\Share::SHARE_TYPE_GROUP]],
- [[\OCP\Share::SHARE_TYPE_LINK]],
- [[\OCP\Share::SHARE_TYPE_REMOTE]],
- [[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP]],
- [[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_LINK]],
- [[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK]],
- [[\OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_LINK]],
- [[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_REMOTE]],
- ];
- }
-}
diff --git a/apps/dav/tests/unit/connector/sabre/tagsplugin.php b/apps/dav/tests/unit/connector/sabre/tagsplugin.php
deleted file mode 100644
index 95ba002e393..00000000000
--- a/apps/dav/tests/unit/connector/sabre/tagsplugin.php
+++ /dev/null
@@ -1,417 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Connector\Sabre;
-
-/**
- * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
- * This file is licensed under the Affero General Public License version 3 or
- * later.
- * See the COPYING-README file.
- */
-class TagsPlugin extends \Test\TestCase {
-
- const TAGS_PROPERTYNAME = \OCA\DAV\Connector\Sabre\TagsPlugin::TAGS_PROPERTYNAME;
- const FAVORITE_PROPERTYNAME = \OCA\DAV\Connector\Sabre\TagsPlugin::FAVORITE_PROPERTYNAME;
- const TAG_FAVORITE = \OCA\DAV\Connector\Sabre\TagsPlugin::TAG_FAVORITE;
-
- /**
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var \Sabre\DAV\Tree
- */
- private $tree;
-
- /**
- * @var \OCP\ITagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\ITags
- */
- private $tagger;
-
- /**
- * @var \OCA\DAV\Connector\Sabre\TagsPlugin
- */
- private $plugin;
-
- public function setUp() {
- parent::setUp();
- $this->server = new \Sabre\DAV\Server();
- $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
- ->disableOriginalConstructor()
- ->getMock();
- $this->tagger = $this->getMock('\OCP\ITags');
- $this->tagManager = $this->getMock('\OCP\ITagManager');
- $this->tagManager->expects($this->any())
- ->method('load')
- ->with('files')
- ->will($this->returnValue($this->tagger));
- $this->plugin = new \OCA\DAV\Connector\Sabre\TagsPlugin($this->tree, $this->tagManager);
- $this->plugin->initialize($this->server);
- }
-
- /**
- * @dataProvider tagsGetPropertiesDataProvider
- */
- public function testGetProperties($tags, $requestedProperties, $expectedProperties) {
- $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node')
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(123));
-
- $expectedCallCount = 0;
- if (count($requestedProperties) > 0) {
- $expectedCallCount = 1;
- }
-
- $this->tagger->expects($this->exactly($expectedCallCount))
- ->method('getTagsForObjects')
- ->with($this->equalTo(array(123)))
- ->will($this->returnValue(array(123 => $tags)));
-
- $propFind = new \Sabre\DAV\PropFind(
- '/dummyPath',
- $requestedProperties,
- 0
- );
-
- $this->plugin->handleGetProperties(
- $propFind,
- $node
- );
-
- $result = $propFind->getResultForMultiStatus();
-
- $this->assertEmpty($result[404]);
- unset($result[404]);
- $this->assertEquals($expectedProperties, $result);
- }
-
- /**
- * @dataProvider tagsGetPropertiesDataProvider
- */
- public function testPreloadThenGetProperties($tags, $requestedProperties, $expectedProperties) {
- $node1 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
- ->disableOriginalConstructor()
- ->getMock();
- $node1->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(111));
- $node2 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
- ->disableOriginalConstructor()
- ->getMock();
- $node2->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(222));
-
- $expectedCallCount = 0;
- if (count($requestedProperties) > 0) {
- // this guarantees that getTagsForObjects
- // is only called once and then the tags
- // are cached
- $expectedCallCount = 1;
- }
-
- $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(123));
- $node->expects($this->exactly($expectedCallCount))
- ->method('getChildren')
- ->will($this->returnValue(array($node1, $node2)));
-
- $this->tagger->expects($this->exactly($expectedCallCount))
- ->method('getTagsForObjects')
- ->with($this->equalTo(array(123, 111, 222)))
- ->will($this->returnValue(
- array(
- 111 => $tags,
- 123 => $tags
- )
- ));
-
- // simulate sabre recursive PROPFIND traversal
- $propFindRoot = new \Sabre\DAV\PropFind(
- '/subdir',
- $requestedProperties,
- 1
- );
- $propFind1 = new \Sabre\DAV\PropFind(
- '/subdir/test.txt',
- $requestedProperties,
- 0
- );
- $propFind2 = new \Sabre\DAV\PropFind(
- '/subdir/test2.txt',
- $requestedProperties,
- 0
- );
-
- $this->plugin->handleGetProperties(
- $propFindRoot,
- $node
- );
- $this->plugin->handleGetProperties(
- $propFind1,
- $node1
- );
- $this->plugin->handleGetProperties(
- $propFind2,
- $node2
- );
-
- $result = $propFind1->getResultForMultiStatus();
-
- $this->assertEmpty($result[404]);
- unset($result[404]);
- $this->assertEquals($expectedProperties, $result);
- }
-
- function tagsGetPropertiesDataProvider() {
- return array(
- // request both, receive both
- array(
- array('tag1', 'tag2', self::TAG_FAVORITE),
- array(self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME),
- array(
- 200 => array(
- self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2')),
- self::FAVORITE_PROPERTYNAME => true,
- )
- )
- ),
- // request tags alone
- array(
- array('tag1', 'tag2', self::TAG_FAVORITE),
- array(self::TAGS_PROPERTYNAME),
- array(
- 200 => array(
- self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2')),
- )
- )
- ),
- // request fav alone
- array(
- array('tag1', 'tag2', self::TAG_FAVORITE),
- array(self::FAVORITE_PROPERTYNAME),
- array(
- 200 => array(
- self::FAVORITE_PROPERTYNAME => true,
- )
- )
- ),
- // request none
- array(
- array('tag1', 'tag2', self::TAG_FAVORITE),
- array(),
- array(
- 200 => array()
- ),
- ),
- // request both with none set, receive both
- array(
- array(),
- array(self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME),
- array(
- 200 => array(
- self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array()),
- self::FAVORITE_PROPERTYNAME => false,
- )
- )
- ),
- );
- }
-
- public function testUpdateTags() {
- // this test will replace the existing tags "tagremove" with "tag1" and "tag2"
- // and keep "tagkeep"
- $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node')
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(123));
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/dummypath')
- ->will($this->returnValue($node));
-
- $this->tagger->expects($this->at(0))
- ->method('getTagsForObjects')
- ->with($this->equalTo(array(123)))
- ->will($this->returnValue(array(123 => array('tagkeep', 'tagremove', self::TAG_FAVORITE))));
-
- // then tag as tag1 and tag2
- $this->tagger->expects($this->at(1))
- ->method('tagAs')
- ->with(123, 'tag1');
- $this->tagger->expects($this->at(2))
- ->method('tagAs')
- ->with(123, 'tag2');
-
- // it will untag tag3
- $this->tagger->expects($this->at(3))
- ->method('unTag')
- ->with(123, 'tagremove');
-
- // properties to set
- $propPatch = new \Sabre\DAV\PropPatch(array(
- self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2', 'tagkeep'))
- ));
-
- $this->plugin->handleUpdateProperties(
- '/dummypath',
- $propPatch
- );
-
- $propPatch->commit();
-
- // all requested properties removed, as they were processed already
- $this->assertEmpty($propPatch->getRemainingMutations());
-
- $result = $propPatch->getResult();
- $this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]);
- $this->assertFalse(isset($result[self::FAVORITE_PROPERTYNAME]));
- }
-
- public function testUpdateTagsFromScratch() {
- $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node')
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(123));
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/dummypath')
- ->will($this->returnValue($node));
-
- $this->tagger->expects($this->at(0))
- ->method('getTagsForObjects')
- ->with($this->equalTo(array(123)))
- ->will($this->returnValue(array()));
-
- // then tag as tag1 and tag2
- $this->tagger->expects($this->at(1))
- ->method('tagAs')
- ->with(123, 'tag1');
- $this->tagger->expects($this->at(2))
- ->method('tagAs')
- ->with(123, 'tag2');
-
- // properties to set
- $propPatch = new \Sabre\DAV\PropPatch(array(
- self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2', 'tagkeep'))
- ));
-
- $this->plugin->handleUpdateProperties(
- '/dummypath',
- $propPatch
- );
-
- $propPatch->commit();
-
- // all requested properties removed, as they were processed already
- $this->assertEmpty($propPatch->getRemainingMutations());
-
- $result = $propPatch->getResult();
- $this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]);
- $this->assertFalse(false, isset($result[self::FAVORITE_PROPERTYNAME]));
- }
-
- public function testUpdateFav() {
- // this test will replace the existing tags "tagremove" with "tag1" and "tag2"
- // and keep "tagkeep"
- $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node')
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(123));
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/dummypath')
- ->will($this->returnValue($node));
-
- // set favorite tag
- $this->tagger->expects($this->once())
- ->method('tagAs')
- ->with(123, self::TAG_FAVORITE);
-
- // properties to set
- $propPatch = new \Sabre\DAV\PropPatch(array(
- self::FAVORITE_PROPERTYNAME => true
- ));
-
- $this->plugin->handleUpdateProperties(
- '/dummypath',
- $propPatch
- );
-
- $propPatch->commit();
-
- // all requested properties removed, as they were processed already
- $this->assertEmpty($propPatch->getRemainingMutations());
-
- $result = $propPatch->getResult();
- $this->assertFalse(false, isset($result[self::TAGS_PROPERTYNAME]));
- $this->assertEquals(200, isset($result[self::FAVORITE_PROPERTYNAME]));
-
- // unfavorite now
- // set favorite tag
- $this->tagger->expects($this->once())
- ->method('unTag')
- ->with(123, self::TAG_FAVORITE);
-
- // properties to set
- $propPatch = new \Sabre\DAV\PropPatch(array(
- self::FAVORITE_PROPERTYNAME => false
- ));
-
- $this->plugin->handleUpdateProperties(
- '/dummypath',
- $propPatch
- );
-
- $propPatch->commit();
-
- // all requested properties removed, as they were processed already
- $this->assertEmpty($propPatch->getRemainingMutations());
-
- $result = $propPatch->getResult();
- $this->assertFalse(false, isset($result[self::TAGS_PROPERTYNAME]));
- $this->assertEquals(200, isset($result[self::FAVORITE_PROPERTYNAME]));
- }
-
-}
diff --git a/apps/dav/tests/unit/dav/groupprincipaltest.php b/apps/dav/tests/unit/dav/groupprincipaltest.php
deleted file mode 100644
index 9d012639310..00000000000
--- a/apps/dav/tests/unit/dav/groupprincipaltest.php
+++ /dev/null
@@ -1,164 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\DAV;
-
-use OCA\DAV\DAV\GroupPrincipalBackend;
-use OCP\IGroupManager;
-use PHPUnit_Framework_MockObject_MockObject;
-use \Sabre\DAV\PropPatch;
-
-class GroupPrincipalTest extends \Test\TestCase {
-
- /** @var IGroupManager | PHPUnit_Framework_MockObject_MockObject */
- private $groupManager;
-
- /** @var GroupPrincipalBackend */
- private $connector;
-
- public function setUp() {
- $this->groupManager = $this->getMockBuilder('\OCP\IGroupManager')
- ->disableOriginalConstructor()->getMock();
-
- $this->connector = new GroupPrincipalBackend($this->groupManager);
- parent::setUp();
- }
-
- public function testGetPrincipalsByPrefixWithoutPrefix() {
- $response = $this->connector->getPrincipalsByPrefix('');
- $this->assertSame([], $response);
- }
-
- public function testGetPrincipalsByPrefixWithUsers() {
- $group1 = $this->mockGroup('foo');
- $group2 = $this->mockGroup('bar');
- $this->groupManager
- ->expects($this->once())
- ->method('search')
- ->with('')
- ->will($this->returnValue([$group1, $group2]));
-
- $expectedResponse = [
- 0 => [
- 'uri' => 'principals/groups/foo',
- '{DAV:}displayname' => 'foo'
- ],
- 1 => [
- 'uri' => 'principals/groups/bar',
- '{DAV:}displayname' => 'bar',
- ]
- ];
- $response = $this->connector->getPrincipalsByPrefix('principals/groups');
- $this->assertSame($expectedResponse, $response);
- }
-
- public function testGetPrincipalsByPrefixEmpty() {
- $this->groupManager
- ->expects($this->once())
- ->method('search')
- ->with('')
- ->will($this->returnValue([]));
-
- $response = $this->connector->getPrincipalsByPrefix('principals/groups');
- $this->assertSame([], $response);
- }
-
- public function testGetPrincipalsByPathWithoutMail() {
- $group1 = $this->mockGroup('foo');
- $this->groupManager
- ->expects($this->once())
- ->method('get')
- ->with('foo')
- ->will($this->returnValue($group1));
-
- $expectedResponse = [
- 'uri' => 'principals/groups/foo',
- '{DAV:}displayname' => 'foo'
- ];
- $response = $this->connector->getPrincipalByPath('principals/groups/foo');
- $this->assertSame($expectedResponse, $response);
- }
-
- public function testGetPrincipalsByPathWithMail() {
- $fooUser = $this->mockGroup('foo');
- $this->groupManager
- ->expects($this->once())
- ->method('get')
- ->with('foo')
- ->will($this->returnValue($fooUser));
-
- $expectedResponse = [
- 'uri' => 'principals/groups/foo',
- '{DAV:}displayname' => 'foo',
- ];
- $response = $this->connector->getPrincipalByPath('principals/groups/foo');
- $this->assertSame($expectedResponse, $response);
- }
-
- public function testGetPrincipalsByPathEmpty() {
- $this->groupManager
- ->expects($this->once())
- ->method('get')
- ->with('foo')
- ->will($this->returnValue(null));
-
- $response = $this->connector->getPrincipalByPath('principals/groups/foo');
- $this->assertSame(null, $response);
- }
-
- public function testGetGroupMemberSet() {
- $response = $this->connector->getGroupMemberSet('principals/groups/foo');
- $this->assertSame([], $response);
- }
-
- public function testGetGroupMembership() {
- $response = $this->connector->getGroupMembership('principals/groups/foo');
- $this->assertSame([], $response);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception
- * @expectedExceptionMessage Setting members of the group is not supported yet
- */
- public function testSetGroupMembership() {
- $this->connector->setGroupMemberSet('principals/groups/foo', ['foo']);
- }
-
- public function testUpdatePrincipal() {
- $this->assertSame(0, $this->connector->updatePrincipal('foo', new PropPatch(array())));
- }
-
- public function testSearchPrincipals() {
- $this->assertSame([], $this->connector->searchPrincipals('principals/groups', []));
- }
-
- /**
- * @return PHPUnit_Framework_MockObject_MockObject
- */
- private function mockGroup($gid) {
- $fooUser = $this->getMockBuilder('\OC\Group\Group')
- ->disableOriginalConstructor()->getMock();
- $fooUser
- ->expects($this->exactly(1))
- ->method('getGID')
- ->will($this->returnValue($gid));
- return $fooUser;
- }
-}
diff --git a/apps/dav/tests/unit/dav/sharing/plugintest.php b/apps/dav/tests/unit/dav/sharing/plugintest.php
deleted file mode 100644
index ce6c96f1bfc..00000000000
--- a/apps/dav/tests/unit/dav/sharing/plugintest.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\DAV;
-
-
-use OCA\DAV\DAV\Sharing\IShareable;
-use OCA\DAV\DAV\Sharing\Plugin;
-use OCA\DAV\Connector\Sabre\Auth;
-use OCP\IRequest;
-use Sabre\DAV\Server;
-use Sabre\DAV\SimpleCollection;
-use Sabre\HTTP\Request;
-use Sabre\HTTP\Response;
-use Test\TestCase;
-
-class PluginTest extends TestCase {
-
- /** @var Plugin */
- private $plugin;
- /** @var Server */
- private $server;
- /** @var IShareable | \PHPUnit_Framework_MockObject_MockObject */
- private $book;
-
- public function setUp() {
- parent::setUp();
-
- /** @var Auth | \PHPUnit_Framework_MockObject_MockObject $authBackend */
- $authBackend = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Auth')->disableOriginalConstructor()->getMock();
- $authBackend->method('isDavAuthenticated')->willReturn(true);
-
- /** @var IRequest $request */
- $request = $this->getMockBuilder('OCP\IRequest')->disableOriginalConstructor()->getMock();
- $this->plugin = new Plugin($authBackend, $request);
-
- $root = new SimpleCollection('root');
- $this->server = new \Sabre\DAV\Server($root);
- /** @var SimpleCollection $node */
- $this->book = $this->getMockBuilder('OCA\DAV\DAV\Sharing\IShareable')->
- disableOriginalConstructor()->
- getMock();
- $this->book->method('getName')->willReturn('addressbook1.vcf');
- $root->addChild($this->book);
- $this->plugin->initialize($this->server);
- }
-
- public function testSharing() {
-
- $this->book->expects($this->once())->method('updateShares')->with([[
- 'href' => 'principal:principals/admin',
- 'commonName' => null,
- 'summary' => null,
- 'readOnly' => false
- ]], ['mailto:wilfredo@example.com']);
-
- // setup request
- $request = new Request();
- $request->addHeader('Content-Type', 'application/xml');
- $request->setUrl('addressbook1.vcf');
- $request->setBody('<?xml version="1.0" encoding="utf-8" ?><CS:share xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns"><CS:set><D:href>principal:principals/admin</D:href><CS:read-write/></CS:set> <CS:remove><D:href>mailto:wilfredo@example.com</D:href></CS:remove></CS:share>');
- $response = new Response();
- $this->plugin->httpPost($request, $response);
- }
-}
diff --git a/apps/dav/tests/unit/dav/systemprincipalbackendtest.php b/apps/dav/tests/unit/dav/systemprincipalbackendtest.php
deleted file mode 100644
index 26717f7509b..00000000000
--- a/apps/dav/tests/unit/dav/systemprincipalbackendtest.php
+++ /dev/null
@@ -1,131 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\DAV;
-
-use OCA\DAV\DAV\SystemPrincipalBackend;
-use Test\TestCase;
-
-class SystemPrincipalBackendTest extends TestCase {
-
- /**
- * @dataProvider providesPrefix
- * @param $expected
- * @param $prefix
- */
- public function testGetPrincipalsByPrefix($expected, $prefix) {
- $backend = new SystemPrincipalBackend();
- $result = $backend->getPrincipalsByPrefix($prefix);
- $this->assertEquals($expected, $result);
- }
-
- public function providesPrefix() {
- return [
- [[], ''],
- [[[
- 'uri' => 'principals/system/system',
- '{DAV:}displayname' => 'system',
- ]], 'principals/system'],
- ];
- }
-
- /**
- * @dataProvider providesPath
- * @param $expected
- * @param $path
- */
- public function testGetPrincipalByPath($expected, $path) {
- $backend = new SystemPrincipalBackend();
- $result = $backend->getPrincipalByPath($path);
- $this->assertEquals($expected, $result);
- }
-
- public function providesPath() {
- return [
- [null, ''],
- [null, 'principals'],
- [null, 'principals/system'],
- [[
- 'uri' => 'principals/system/system',
- '{DAV:}displayname' => 'system',
- ], 'principals/system/system'],
- ];
- }
-
- /**
- * @dataProvider providesPrincipalForGetGroupMemberSet
- * @expectedException \Sabre\DAV\Exception
- * @expectedExceptionMessage Principal not found
- *
- * @param string $principal
- * @throws \Sabre\DAV\Exception
- */
- public function testGetGroupMemberSetExceptional($principal) {
- $backend = new SystemPrincipalBackend();
- $backend->getGroupMemberSet($principal);
- }
-
- public function providesPrincipalForGetGroupMemberSet() {
- return [
- [null],
- ['principals/system'],
- ];
- }
-
- /**
- * @throws \Sabre\DAV\Exception
- */
- public function testGetGroupMemberSet() {
- $backend = new SystemPrincipalBackend();
- $result = $backend->getGroupMemberSet('principals/system/system');
- $this->assertEquals(['principals/system/system'], $result);
- }
-
- /**
- * @dataProvider providesPrincipalForGetGroupMembership
- * @expectedException \Sabre\DAV\Exception
- * @expectedExceptionMessage Principal not found
- *
- * @param string $principal
- * @throws \Sabre\DAV\Exception
- */
- public function testGetGroupMembershipExceptional($principal) {
- $backend = new SystemPrincipalBackend();
- $backend->getGroupMembership($principal);
- }
-
- public function providesPrincipalForGetGroupMembership() {
- return [
- ['principals/system/a'],
- ];
- }
-
- /**
- * @throws \Sabre\DAV\Exception
- */
- public function testGetGroupMembership() {
- $backend = new SystemPrincipalBackend();
- $result = $backend->getGroupMembership('principals/system/system');
- $this->assertEquals([], $result);
- }
-
-
-}
diff --git a/apps/dav/tests/unit/migration/addressbookadaptertest.php b/apps/dav/tests/unit/migration/addressbookadaptertest.php
deleted file mode 100644
index e6e57049a93..00000000000
--- a/apps/dav/tests/unit/migration/addressbookadaptertest.php
+++ /dev/null
@@ -1,129 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Migration;
-
-use DomainException;
-use OCA\Dav\Migration\AddressBookAdapter;
-use OCP\IDBConnection;
-use Test\TestCase;
-
-/**
- * Class AddressbookAdapterTest
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\Unit\Migration
- */
-class AddressbookAdapterTest extends TestCase {
-
- /** @var IDBConnection */
- private $db;
- /** @var AddressBookAdapter */
- private $adapter;
- /** @var array */
- private $books = [];
- /** @var array */
- private $cards = [];
-
- public function setUp() {
- parent::setUp();
- $this->db = \OC::$server->getDatabaseConnection();
-
- $manager = new \OC\DB\MDB2SchemaManager($this->db);
- $manager->createDbFromStructure(__DIR__ . '/contacts_schema.xml');
-
- $this->adapter = new AddressBookAdapter($this->db);
- }
-
- public function tearDown() {
- $this->db->dropTable('contacts_addressbooks');
- $this->db->dropTable('contacts_cards');
- parent::tearDown();
- }
-
- /**
- * @expectedException DomainException
- */
- public function testOldTablesDoNotExist() {
- $adapter = new AddressBookAdapter(\OC::$server->getDatabaseConnection(), 'crazy_table_that_does_no_exist');
- $adapter->setup();
- }
-
- public function test() {
-
- // insert test data
- $builder = $this->db->getQueryBuilder();
- $builder->insert('contacts_addressbooks')
- ->values([
- 'userid' => $builder->createNamedParameter('test-user-666'),
- 'displayname' => $builder->createNamedParameter('Display Name'),
- 'uri' => $builder->createNamedParameter('contacts'),
- 'description' => $builder->createNamedParameter('An address book for testing'),
- 'ctag' => $builder->createNamedParameter('112233'),
- 'active' => $builder->createNamedParameter('1')
- ])
- ->execute();
- $builder = $this->db->getQueryBuilder();
- $builder->insert('contacts_cards')
- ->values([
- 'addressbookid' => $builder->createNamedParameter(6666),
- 'fullname' => $builder->createNamedParameter('Full Name'),
- 'carddata' => $builder->createNamedParameter('datadatadata'),
- 'uri' => $builder->createNamedParameter('some-card.vcf'),
- 'lastmodified' => $builder->createNamedParameter('112233'),
- ])
- ->execute();
- $builder = $this->db->getQueryBuilder();
- $builder->insert('share')
- ->values([
- 'share_type' => $builder->createNamedParameter(1),
- 'share_with' => $builder->createNamedParameter('user01'),
- 'uid_owner' => $builder->createNamedParameter('user02'),
- 'item_type' => $builder->createNamedParameter('addressbook'),
- 'item_source' => $builder->createNamedParameter(6666),
- 'item_target' => $builder->createNamedParameter('Contacts (user02)'),
- ])
- ->execute();
-
- // test the adapter
- $this->adapter->foreachBook('test-user-666', function($row) {
- $this->books[] = $row;
- });
- $this->assertArrayHasKey('id', $this->books[0]);
- $this->assertEquals('test-user-666', $this->books[0]['userid']);
- $this->assertEquals('Display Name', $this->books[0]['displayname']);
- $this->assertEquals('contacts', $this->books[0]['uri']);
- $this->assertEquals('An address book for testing', $this->books[0]['description']);
- $this->assertEquals('112233', $this->books[0]['ctag']);
-
- $this->adapter->foreachCard(6666, function($row) {
- $this->cards[]= $row;
- });
- $this->assertArrayHasKey('id', $this->cards[0]);
- $this->assertEquals(6666, $this->cards[0]['addressbookid']);
-
- // test getShares
- $shares = $this->adapter->getShares(6666);
- $this->assertEquals(1, count($shares));
-
- }
-
-}
diff --git a/apps/dav/tests/unit/migration/calendar_schema.xml b/apps/dav/tests/unit/migration/calendar_schema.xml
deleted file mode 100644
index 6c88b596a3f..00000000000
--- a/apps/dav/tests/unit/migration/calendar_schema.xml
+++ /dev/null
@@ -1,191 +0,0 @@
-<?xml version="1.0" encoding="ISO-8859-1" ?>
-<database>
-
- <name>*dbname*</name>
- <create>true</create>
- <overwrite>false</overwrite>
-
- <charset>utf8</charset>
-
- <table>
-
- <name>*dbprefix*clndr_objects</name>
-
- <declaration>
-
- <field>
- <name>id</name>
- <type>integer</type>
- <default>0</default>
- <notnull>true</notnull>
- <autoincrement>1</autoincrement>
- <unsigned>true</unsigned>
- <length>4</length>
- </field>
-
- <field>
- <name>calendarid</name>
- <type>integer</type>
- <default></default>
- <notnull>true</notnull>
- <unsigned>true</unsigned>
- <length>4</length>
- </field>
-
- <field>
- <name>objecttype</name>
- <type>text</type>
- <default></default>
- <notnull>true</notnull>
- <length>40</length>
- </field>
-
- <field>
- <name>startdate</name>
- <type>timestamp</type>
- <default>1970-01-01 00:00:00</default>
- <notnull>false</notnull>
- </field>
-
- <field>
- <name>enddate</name>
- <type>timestamp</type>
- <default>1970-01-01 00:00:00</default>
- <notnull>false</notnull>
- </field>
-
- <field>
- <name>repeating</name>
- <type>integer</type>
- <default></default>
- <notnull>false</notnull>
- <length>4</length>
- </field>
-
- <field>
- <name>summary</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>255</length>
- </field>
-
- <field>
- <name>calendardata</name>
- <type>clob</type>
- <notnull>false</notnull>
- </field>
-
- <field>
- <name>uri</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>255</length>
- </field>
-
- <field>
- <name>lastmodified</name>
- <type>integer</type>
- <default></default>
- <notnull>false</notnull>
- <length>4</length>
- </field>
-
- </declaration>
-
- </table>
-
- <table>
-
- <name>*dbprefix*clndr_calendars</name>
-
- <declaration>
-
- <field>
- <name>id</name>
- <type>integer</type>
- <default>0</default>
- <notnull>true</notnull>
- <autoincrement>1</autoincrement>
- <unsigned>true</unsigned>
- <length>4</length>
- </field>
-
- <field>
- <name>userid</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>255</length>
- </field>
-
- <field>
- <name>displayname</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>100</length>
- </field>
-
- <field>
- <name>uri</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>255</length>
- </field>
-
- <field>
- <name>active</name>
- <type>integer</type>
- <default>1</default>
- <notnull>true</notnull>
- <length>4</length>
- </field>
-
- <field>
- <name>ctag</name>
- <type>integer</type>
- <default>0</default>
- <notnull>true</notnull>
- <unsigned>true</unsigned>
- <length>4</length>
- </field>
-
- <field>
- <name>calendarorder</name>
- <type>integer</type>
- <default>0</default>
- <notnull>true</notnull>
- <unsigned>true</unsigned>
- <length>4</length>
- </field>
-
- <field>
- <name>calendarcolor</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>10</length>
- </field>
-
- <field>
- <name>timezone</name>
- <type>clob</type>
- <notnull>false</notnull>
- </field>
-
- <field>
- <name>components</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>100</length>
- </field>
-
- </declaration>
-
- </table>
-
-</database>
diff --git a/apps/dav/tests/unit/migration/calendaradaptertest.php b/apps/dav/tests/unit/migration/calendaradaptertest.php
deleted file mode 100644
index f92774ef6ad..00000000000
--- a/apps/dav/tests/unit/migration/calendaradaptertest.php
+++ /dev/null
@@ -1,131 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Migration;
-
-use DomainException;
-use OCA\Dav\Migration\AddressBookAdapter;
-use OCA\Dav\Migration\CalendarAdapter;
-use OCP\IDBConnection;
-use Test\TestCase;
-
-/**
- * Class CalendarAdapterTest
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\Unit\Migration
- */
-class CalendarAdapterTest extends TestCase {
-
- /** @var IDBConnection */
- private $db;
- /** @var CalendarAdapter */
- private $adapter;
- /** @var array */
- private $cals = [];
- /** @var array */
- private $calObjs = [];
-
- public function setUp() {
- parent::setUp();
- $this->db = \OC::$server->getDatabaseConnection();
-
- $manager = new \OC\DB\MDB2SchemaManager($this->db);
- $manager->createDbFromStructure(__DIR__ . '/calendar_schema.xml');
-
- $this->adapter = new CalendarAdapter($this->db);
- }
-
- public function tearDown() {
- $this->db->dropTable('clndr_calendars');
- $this->db->dropTable('clndr_objects');
- parent::tearDown();
- }
-
- /**
- * @expectedException DomainException
- */
- public function testOldTablesDoNotExist() {
- $adapter = new AddressBookAdapter(\OC::$server->getDatabaseConnection(), 'crazy_table_that_does_no_exist');
- $adapter->setup();
- }
-
- public function test() {
-
- // insert test data
- $builder = $this->db->getQueryBuilder();
- $builder->insert('clndr_calendars')
- ->values([
- 'userid' => $builder->createNamedParameter('test-user-666'),
- 'displayname' => $builder->createNamedParameter('Display Name'),
- 'uri' => $builder->createNamedParameter('events'),
- 'ctag' => $builder->createNamedParameter('112233'),
- 'active' => $builder->createNamedParameter('1')
- ])
- ->execute();
- $builder = $this->db->getQueryBuilder();
- $builder->insert('clndr_objects')
- ->values([
- 'calendarid' => $builder->createNamedParameter(6666),
- 'objecttype' => $builder->createNamedParameter('VEVENT'),
- 'startdate' => $builder->createNamedParameter(new \DateTime(), 'datetime'),
- 'enddate' => $builder->createNamedParameter(new \DateTime(), 'datetime'),
- 'repeating' => $builder->createNamedParameter(0),
- 'summary' => $builder->createNamedParameter('Something crazy will happen'),
- 'uri' => $builder->createNamedParameter('event.ics'),
- 'lastmodified' => $builder->createNamedParameter('112233'),
- ])
- ->execute();
- $builder = $this->db->getQueryBuilder();
- $builder->insert('share')
- ->values([
- 'share_type' => $builder->createNamedParameter(1),
- 'share_with' => $builder->createNamedParameter('user01'),
- 'uid_owner' => $builder->createNamedParameter('user02'),
- 'item_type' => $builder->createNamedParameter('calendar'),
- 'item_source' => $builder->createNamedParameter(6666),
- 'item_target' => $builder->createNamedParameter('Contacts (user02)'),
- ])
- ->execute();
-
- // test the adapter
- $this->adapter->foreachCalendar('test-user-666', function($row) {
- $this->cals[] = $row;
- });
- $this->assertArrayHasKey('id', $this->cals[0]);
- $this->assertEquals('test-user-666', $this->cals[0]['userid']);
- $this->assertEquals('Display Name', $this->cals[0]['displayname']);
- $this->assertEquals('events', $this->cals[0]['uri']);
- $this->assertEquals('112233', $this->cals[0]['ctag']);
-
- $this->adapter->foreachCalendarObject(6666, function($row) {
- $this->calObjs[]= $row;
- });
- $this->assertArrayHasKey('id', $this->calObjs[0]);
- $this->assertEquals(6666, $this->calObjs[0]['calendarid']);
-
- // test getShares
- $shares = $this->adapter->getShares(6666);
- $this->assertEquals(1, count($shares));
-
- }
-
-}
diff --git a/apps/dav/tests/unit/migration/contacts_schema.xml b/apps/dav/tests/unit/migration/contacts_schema.xml
deleted file mode 100644
index 51836a1e0c6..00000000000
--- a/apps/dav/tests/unit/migration/contacts_schema.xml
+++ /dev/null
@@ -1,151 +0,0 @@
-<?xml version="1.0" encoding="ISO-8859-1" ?>
-<database>
-
- <name>*dbname*</name>
- <create>true</create>
- <overwrite>false</overwrite>
- <charset>utf8</charset>
- <table>
-
- <name>*dbprefix*contacts_addressbooks</name>
-
- <declaration>
-
- <field>
- <name>id</name>
- <type>integer</type>
- <default>0</default>
- <notnull>true</notnull>
- <autoincrement>1</autoincrement>
- <unsigned>true</unsigned>
- <length>4</length>
- </field>
-
- <field>
- <name>userid</name>
- <type>text</type>
- <default></default>
- <notnull>true</notnull>
- <length>255</length>
- </field>
-
- <field>
- <name>displayname</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>255</length>
- </field>
-
- <field>
- <name>uri</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>200</length>
- </field>
-
- <field>
- <name>description</name>
- <type>text</type>
- <notnull>false</notnull>
- <length>255</length>
- </field>
-
- <field>
- <name>ctag</name>
- <type>integer</type>
- <default>1</default>
- <notnull>true</notnull>
- <unsigned>true</unsigned>
- <length>4</length>
- </field>
-
- <field>
- <name>active</name>
- <type>integer</type>
- <default>1</default>
- <notnull>true</notnull>
- <length>4</length>
- </field>
-
- <index>
- <name>c_addressbook_userid_index</name>
- <field>
- <name>userid</name>
- <sorting>ascending</sorting>
- </field>
- </index>
- </declaration>
-
- </table>
-
- <table>
-
- <name>*dbprefix*contacts_cards</name>
-
- <declaration>
-
- <field>
- <name>id</name>
- <type>integer</type>
- <default>0</default>
- <notnull>true</notnull>
- <autoincrement>1</autoincrement>
- <unsigned>true</unsigned>
- <length>4</length>
- </field>
-
- <field>
- <name>addressbookid</name>
- <type>integer</type>
- <default></default>
- <notnull>true</notnull>
- <unsigned>true</unsigned>
- <length>4</length>
- </field>
-
- <field>
- <name>fullname</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>255</length>
- </field>
-
- <field>
- <name>carddata</name>
- <type>clob</type>
- <notnull>false</notnull>
- </field>
-
- <field>
- <name>uri</name>
- <type>text</type>
- <default></default>
- <notnull>false</notnull>
- <length>200</length>
- </field>
-
- <field>
- <name>lastmodified</name>
- <type>integer</type>
- <default></default>
- <notnull>false</notnull>
- <unsigned>true</unsigned>
- <length>4</length>
- </field>
-
-
- <index>
- <name>c_addressbookid_index</name>
- <field>
- <name>addressbookid</name>
- <sorting>ascending</sorting>
- </field>
- </index>
- </declaration>
-
- </table>
-
-</database>
diff --git a/apps/dav/tests/unit/migration/migrateaddressbooktest.php b/apps/dav/tests/unit/migration/migrateaddressbooktest.php
deleted file mode 100644
index 31cb16265c0..00000000000
--- a/apps/dav/tests/unit/migration/migrateaddressbooktest.php
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Migration;
-
-use OCA\DAV\CardDAV\CardDavBackend;
-use OCA\Dav\Migration\AddressBookAdapter;
-use OCP\ILogger;
-use Test\TestCase;
-
-class MigrateAddressbookTest extends TestCase {
-
- public function testMigration() {
- /** @var AddressBookAdapter | \PHPUnit_Framework_MockObject_MockObject $adapter */
- $adapter = $this->mockAdapter([
- ['share_type' => '1', 'share_with' => 'users', 'permissions' => '31'],
- ['share_type' => '2', 'share_with' => 'adam', 'permissions' => '1'],
- ]);
-
- /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $cardDav */
- $cardDav = $this->getMockBuilder('\OCA\Dav\CardDAV\CardDAVBackend')->disableOriginalConstructor()->getMock();
- $cardDav->expects($this->any())->method('createAddressBook')->willReturn(666);
- $cardDav->expects($this->any())->method('getAddressBookById')->willReturn([]);
- $cardDav->expects($this->once())->method('createAddressBook')->with('principals/users/test01', 'test_contacts');
- $cardDav->expects($this->once())->method('createCard')->with(666, '63f0dd6c-39d5-44be-9d34-34e7a7441fc2.vcf', 'BEGIN:VCARD');
- $cardDav->expects($this->once())->method('updateShares')->with($this->anything(), [
- ['href' => 'principal:principals/groups/users', 'readOnly' => false],
- ['href' => 'principal:principals/users/adam', 'readOnly' => true]
- ]);
- /** @var ILogger $logger */
- $logger = $this->getMockBuilder('\OCP\ILogger')->disableOriginalConstructor()->getMock();
-
- $m = new \OCA\Dav\Migration\MigrateAddressbooks($adapter, $cardDav, $logger, null);
- $m->migrateForUser('test01');
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject
- */
- private function mockAdapter($shares = []) {
- $adapter = $this->getMockBuilder('\OCA\Dav\Migration\AddressBookAdapter')->disableOriginalConstructor()->getMock();
- $adapter->expects($this->any())->method('foreachBook')->willReturnCallback(function ($user, \Closure $callBack) {
- $callBack([
- 'id' => 0,
- 'userid' => $user,
- 'displayname' => 'Test Contacts',
- 'uri' => 'test_contacts',
- 'description' => 'Contacts to test with',
- 'ctag' => 1234567890,
- 'active' => 1
- ]);
- });
- $adapter->expects($this->any())->method('foreachCard')->willReturnCallback(function ($addressBookId, \Closure $callBack) {
- $callBack([
- 'userid' => $addressBookId,
- 'uri' => '63f0dd6c-39d5-44be-9d34-34e7a7441fc2.vcf',
- 'carddata' => 'BEGIN:VCARD'
- ]);
- });
- $adapter->expects($this->any())->method('getShares')->willReturn($shares);
- return $adapter;
- }
-
-}
diff --git a/apps/dav/tests/unit/migration/migratecalendartest.php b/apps/dav/tests/unit/migration/migratecalendartest.php
deleted file mode 100644
index e62970aef34..00000000000
--- a/apps/dav/tests/unit/migration/migratecalendartest.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-/**
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit\Migration;
-
-use OCA\DAV\CalDAV\CalDavBackend;
-use OCA\Dav\Migration\CalendarAdapter;
-use OCP\ILogger;
-use Test\TestCase;
-
-class MigrateCalendarTest extends TestCase {
-
- public function testMigration() {
- /** @var CalendarAdapter | \PHPUnit_Framework_MockObject_MockObject $adapter */
- $adapter = $this->mockAdapter([
- ['share_type' => '1', 'share_with' => 'users', 'permissions' => '31'],
- ['share_type' => '2', 'share_with' => 'adam', 'permissions' => '1'],
- ]);
-
- /** @var CalDavBackend | \PHPUnit_Framework_MockObject_MockObject $cardDav */
- $cardDav = $this->getMockBuilder('\OCA\Dav\CalDAV\CalDAVBackend')->disableOriginalConstructor()->getMock();
- $cardDav->expects($this->any())->method('createCalendar')->willReturn(666);
- $cardDav->expects($this->once())->method('createCalendar')->with('principals/users/test01', 'test_contacts');
- $cardDav->expects($this->once())->method('createCalendarObject')->with(666, '63f0dd6c-39d5-44be-9d34-34e7a7441fc2.ics', 'BEGIN:VCARD');
- $cardDav->expects($this->once())->method('updateShares')->with($this->anything(), [
- ['href' => 'principal:principals/groups/users', 'readOnly' => false],
- ['href' => 'principal:principals/users/adam', 'readOnly' => true]
- ]);
- /** @var ILogger $logger */
- $logger = $this->getMockBuilder('\OCP\ILogger')->disableOriginalConstructor()->getMock();
-
- $m = new \OCA\Dav\Migration\MigrateCalendars($adapter, $cardDav, $logger, null);
- $m->migrateForUser('test01');
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject
- */
- private function mockAdapter($shares = [], $calData = 'BEGIN:VCARD') {
- $adapter = $this->getMockBuilder('\OCA\Dav\Migration\CalendarAdapter')
- ->disableOriginalConstructor()
- ->getMock();
- $adapter->expects($this->any())->method('foreachCalendar')->willReturnCallback(function ($user, \Closure $callBack) {
- $callBack([
- // calendarorder | calendarcolor | timezone | components
- 'id' => 0,
- 'userid' => $user,
- 'displayname' => 'Test Contacts',
- 'uri' => 'test_contacts',
- 'ctag' => 1234567890,
- 'active' => 1,
- 'calendarorder' => '0',
- 'calendarcolor' => '#b3dc6c',
- 'timezone' => null,
- 'components' => 'VEVENT,VTODO,VJOURNAL'
- ]);
- });
- $adapter->expects($this->any())->method('foreachCalendarObject')->willReturnCallback(function ($addressBookId, \Closure $callBack) use ($calData) {
- $callBack([
- 'userid' => $addressBookId,
- 'uri' => '63f0dd6c-39d5-44be-9d34-34e7a7441fc2.ics',
- 'calendardata' => $calData
- ]);
- });
- $adapter->expects($this->any())->method('getShares')->willReturn($shares);
- return $adapter;
- }
-}
diff --git a/apps/dav/tests/unit/phpunit.xml b/apps/dav/tests/unit/phpunit.xml
index 314855d863b..c85d07c6fcb 100644
--- a/apps/dav/tests/unit/phpunit.xml
+++ b/apps/dav/tests/unit/phpunit.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2015-2017 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+ -->
<phpunit bootstrap="bootstrap.php"
verbose="true"
timeoutForSmallTests="900"
@@ -6,14 +10,14 @@
timeoutForLargeTests="900"
>
<testsuite name='unit'>
- <directory suffix='.php'>.</directory>
+ <directory suffix='Test.php'>.</directory>
</testsuite>
<!-- filters for code coverage -->
<filter>
<whitelist>
- <directory suffix=".php">../../dav</directory>
+ <directory suffix=".php">../../../dav</directory>
<exclude>
- <directory suffix=".php">../../dav/tests</directory>
+ <directory suffix=".php">../../../dav/tests</directory>
</exclude>
</whitelist>
</filter>
@@ -22,4 +26,3 @@
<log type="coverage-clover" target="./clover.xml"/>
</logging>
</phpunit>
-
diff --git a/apps/dav/tests/unit/servertest.php b/apps/dav/tests/unit/servertest.php
deleted file mode 100644
index b25da3cc807..00000000000
--- a/apps/dav/tests/unit/servertest.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-/**
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Tests\Unit;
-
-use OCA\DAV\Server;
-use OCP\IRequest;
-
-/**
- * Class ServerTest
- *
- * @group DB
- *
- * @package OCA\DAV\Tests\Unit
- */
-class ServerTest extends \Test\TestCase {
-
- public function test() {
- /** @var IRequest $r */
- $r = $this->getMockBuilder('\OCP\IRequest')
- ->disableOriginalConstructor()->getMock();
- $s = new Server($r, '/');
- $this->assertNotNull($s->server);
- }
-} \ No newline at end of file
diff --git a/apps/dav/tests/unit/systemtag/systemtagmappingnode.php b/apps/dav/tests/unit/systemtag/systemtagmappingnode.php
deleted file mode 100644
index 7f2ff7d6616..00000000000
--- a/apps/dav/tests/unit/systemtag/systemtagmappingnode.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-/**
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\SystemTag;
-
-use Sabre\DAV\Exception\NotFound;
-use OC\SystemTag\SystemTag;
-use OCP\SystemTag\TagNotFoundException;
-
-class SystemTagMappingNode extends \Test\TestCase {
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\SystemTag\ISystemTagObjectMapper
- */
- private $tagMapper;
-
- protected function setUp() {
- parent::setUp();
-
- $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager');
- $this->tagMapper = $this->getMock('\OCP\SystemTag\ISystemTagObjectMapper');
- }
-
- public function getMappingNode($isAdmin = true, $tag = null) {
- if ($tag === null) {
- $tag = new SystemTag(1, 'Test', true, true);
- }
- return new \OCA\DAV\SystemTag\SystemTagMappingNode(
- $tag,
- 123,
- 'files',
- $isAdmin,
- $this->tagManager,
- $this->tagMapper
- );
- }
-
- public function testGetters() {
- $tag = new SystemTag(1, 'Test', true, false);
- $node = $this->getMappingNode(true, $tag);
- $this->assertEquals('1', $node->getName());
- $this->assertEquals($tag, $node->getSystemTag());
- $this->assertEquals(123, $node->getObjectId());
- $this->assertEquals('files', $node->getObjectType());
- }
-
- public function adminFlagProvider() {
- return [[true], [false]];
- }
-
- /**
- * @dataProvider adminFlagProvider
- */
- public function testDeleteTag($isAdmin) {
- $this->tagManager->expects($this->never())
- ->method('deleteTags');
- $this->tagMapper->expects($this->once())
- ->method('unassignTags')
- ->with(123, 'files', 1);
-
- $this->getMappingNode($isAdmin)->delete();
- }
-
- public function tagNodeDeleteProviderPermissionException() {
- return [
- [
- // cannot unassign invisible tag
- new SystemTag(1, 'Original', false, true),
- 'Sabre\DAV\Exception\NotFound',
- ],
- [
- // cannot unassign non-assignable tag
- new SystemTag(1, 'Original', true, false),
- 'Sabre\DAV\Exception\Forbidden',
- ],
- ];
- }
-
- /**
- * @dataProvider tagNodeDeleteProviderPermissionException
- */
- public function testDeleteTagExpectedException($tag, $expectedException) {
- $this->tagManager->expects($this->never())
- ->method('deleteTags');
- $this->tagMapper->expects($this->never())
- ->method('unassignTags');
-
- $thrown = null;
- try {
- $this->getMappingNode(false, $tag)->delete();
- } catch (\Exception $e) {
- $thrown = $e;
- }
-
- $this->assertInstanceOf($expectedException, $thrown);
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\NotFound
- */
- public function testDeleteTagNotFound() {
- $this->tagMapper->expects($this->once())
- ->method('unassignTags')
- ->with(123, 'files', 1)
- ->will($this->throwException(new TagNotFoundException()));
-
- $this->getMappingNode()->delete();
- }
-}
diff --git a/apps/dav/tests/unit/systemtag/systemtagnode.php b/apps/dav/tests/unit/systemtag/systemtagnode.php
deleted file mode 100644
index 5184b74e5c8..00000000000
--- a/apps/dav/tests/unit/systemtag/systemtagnode.php
+++ /dev/null
@@ -1,244 +0,0 @@
-<?php
-/**
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\SystemTag;
-
-use Sabre\DAV\Exception\NotFound;
-use Sabre\DAV\Exception\MethodNotAllowed;
-use Sabre\DAV\Exception\Conflict;
-
-use OC\SystemTag\SystemTag;
-use OCP\SystemTag\TagNotFoundException;
-use OCP\SystemTag\TagAlreadyExistsException;
-
-class SystemTagNode extends \Test\TestCase {
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- protected function setUp() {
- parent::setUp();
-
- $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager');
- }
-
- protected function getTagNode($isAdmin = true, $tag = null) {
- if ($tag === null) {
- $tag = new SystemTag(1, 'Test', true, true);
- }
- return new \OCA\DAV\SystemTag\SystemTagNode(
- $tag,
- $isAdmin,
- $this->tagManager
- );
- }
-
- public function adminFlagProvider() {
- return [[true], [false]];
- }
-
- /**
- * @dataProvider adminFlagProvider
- */
- public function testGetters($isAdmin) {
- $tag = new SystemTag('1', 'Test', true, true);
- $node = $this->getTagNode($isAdmin, $tag);
- $this->assertEquals('1', $node->getName());
- $this->assertEquals($tag, $node->getSystemTag());
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\MethodNotAllowed
- */
- public function testSetName() {
- $this->getTagNode()->setName('2');
- }
-
- public function tagNodeProvider() {
- return [
- // admin
- [
- true,
- new SystemTag(1, 'Original', true, true),
- ['Renamed', true, true]
- ],
- [
- true,
- new SystemTag(1, 'Original', true, true),
- ['Original', false, false]
- ],
- // non-admin
- [
- // renaming allowed
- false,
- new SystemTag(1, 'Original', true, true),
- ['Rename', true, true]
- ],
- ];
- }
-
- /**
- * @dataProvider tagNodeProvider
- */
- public function testUpdateTag($isAdmin, $originalTag, $changedArgs) {
- $this->tagManager->expects($this->once())
- ->method('updateTag')
- ->with(1, $changedArgs[0], $changedArgs[1], $changedArgs[2]);
- $this->getTagNode($isAdmin, $originalTag)
- ->update($changedArgs[0], $changedArgs[1], $changedArgs[2]);
- }
-
- public function tagNodeProviderPermissionException() {
- return [
- [
- // changing permissions not allowed
- new SystemTag(1, 'Original', true, true),
- ['Original', false, true],
- 'Sabre\DAV\Exception\Forbidden',
- ],
- [
- // changing permissions not allowed
- new SystemTag(1, 'Original', true, true),
- ['Original', true, false],
- 'Sabre\DAV\Exception\Forbidden',
- ],
- [
- // changing permissions not allowed
- new SystemTag(1, 'Original', true, true),
- ['Original', false, false],
- 'Sabre\DAV\Exception\Forbidden',
- ],
- [
- // changing non-assignable not allowed
- new SystemTag(1, 'Original', true, false),
- ['Rename', true, false],
- 'Sabre\DAV\Exception\Forbidden',
- ],
- [
- // changing non-assignable not allowed
- new SystemTag(1, 'Original', true, false),
- ['Original', true, true],
- 'Sabre\DAV\Exception\Forbidden',
- ],
- [
- // invisible tag does not exist
- new SystemTag(1, 'Original', false, false),
- ['Rename', false, false],
- 'Sabre\DAV\Exception\NotFound',
- ],
- ];
- }
-
- /**
- * @dataProvider tagNodeProviderPermissionException
- */
- public function testUpdateTagPermissionException($originalTag, $changedArgs, $expectedException = null) {
- $this->tagManager->expects($this->never())
- ->method('updateTag');
-
- $thrown = null;
-
- try {
- $this->getTagNode(false, $originalTag)
- ->update($changedArgs[0], $changedArgs[1], $changedArgs[2]);
- } catch (\Exception $e) {
- $thrown = $e;
- }
-
- $this->assertInstanceOf($expectedException, $thrown);
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\Conflict
- */
- public function testUpdateTagAlreadyExists() {
- $this->tagManager->expects($this->once())
- ->method('updateTag')
- ->with(1, 'Renamed', false, true)
- ->will($this->throwException(new TagAlreadyExistsException()));
- $this->getTagNode()->update('Renamed', false, true);
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\NotFound
- */
- public function testUpdateTagNotFound() {
- $this->tagManager->expects($this->once())
- ->method('updateTag')
- ->with(1, 'Renamed', false, true)
- ->will($this->throwException(new TagNotFoundException()));
- $this->getTagNode()->update('Renamed', false, true);
- }
-
- /**
- * @dataProvider adminFlagProvider
- */
- public function testDeleteTag($isAdmin) {
- $this->tagManager->expects($this->once())
- ->method('deleteTags')
- ->with('1');
- $this->getTagNode($isAdmin)->delete();
- }
-
- public function tagNodeDeleteProviderPermissionException() {
- return [
- [
- // cannot delete invisible tag
- new SystemTag(1, 'Original', false, true),
- 'Sabre\DAV\Exception\NotFound',
- ],
- [
- // cannot delete non-assignable tag
- new SystemTag(1, 'Original', true, false),
- 'Sabre\DAV\Exception\Forbidden',
- ],
- ];
- }
-
- /**
- * @dataProvider tagNodeDeleteProviderPermissionException
- */
- public function testDeleteTagPermissionException($tag, $expectedException) {
- $this->tagManager->expects($this->never())
- ->method('deleteTags');
-
- try {
- $this->getTagNode(false, $tag)->delete();
- } catch (\Exception $e) {
- $thrown = $e;
- }
-
- $this->assertInstanceOf($expectedException, $thrown);
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\NotFound
- */
- public function testDeleteTagNotFound() {
- $this->tagManager->expects($this->once())
- ->method('deleteTags')
- ->with('1')
- ->will($this->throwException(new TagNotFoundException()));
- $this->getTagNode()->delete();
- }
-}
diff --git a/apps/dav/tests/unit/systemtag/systemtagplugin.php b/apps/dav/tests/unit/systemtag/systemtagplugin.php
deleted file mode 100644
index 4466533f1e0..00000000000
--- a/apps/dav/tests/unit/systemtag/systemtagplugin.php
+++ /dev/null
@@ -1,608 +0,0 @@
-<?php
-/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\SystemTag;
-
-use OC\SystemTag\SystemTag;
-use OCP\IGroupManager;
-use OCP\IUserSession;
-use OCP\SystemTag\TagAlreadyExistsException;
-
-class SystemTagPlugin extends \Test\TestCase {
-
- const ID_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::ID_PROPERTYNAME;
- const DISPLAYNAME_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::DISPLAYNAME_PROPERTYNAME;
- const USERVISIBLE_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::USERVISIBLE_PROPERTYNAME;
- const USERASSIGNABLE_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::USERASSIGNABLE_PROPERTYNAME;
-
- /**
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var \Sabre\DAV\Tree
- */
- private $tree;
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var IGroupManager
- */
- private $groupManager;
-
- /**
- * @var IUserSession
- */
- private $userSession;
-
- /**
- * @var \OCA\DAV\SystemTag\SystemTagPlugin
- */
- private $plugin;
-
- public function setUp() {
- parent::setUp();
- $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->server = new \Sabre\DAV\Server($this->tree);
-
- $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager');
- $this->groupManager = $this->getMock('\OCP\IGroupManager');
- $this->userSession = $this->getMock('\OCP\IUserSession');
-
- $this->plugin = new \OCA\DAV\SystemTag\SystemTagPlugin(
- $this->tagManager,
- $this->groupManager,
- $this->userSession
- );
- $this->plugin->initialize($this->server);
- }
-
- public function testGetProperties() {
- $systemTag = new SystemTag(1, 'Test', true, true);
- $requestedProperties = [
- self::ID_PROPERTYNAME,
- self::DISPLAYNAME_PROPERTYNAME,
- self::USERVISIBLE_PROPERTYNAME,
- self::USERASSIGNABLE_PROPERTYNAME
- ];
- $expectedProperties = [
- 200 => [
- self::ID_PROPERTYNAME => '1',
- self::DISPLAYNAME_PROPERTYNAME => 'Test',
- self::USERVISIBLE_PROPERTYNAME => 'true',
- self::USERASSIGNABLE_PROPERTYNAME => 'true',
- ]
- ];
-
- $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagNode')
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getSystemTag')
- ->will($this->returnValue($systemTag));
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/systemtag/1')
- ->will($this->returnValue($node));
-
- $propFind = new \Sabre\DAV\PropFind(
- '/systemtag/1',
- $requestedProperties,
- 0
- );
-
- $this->plugin->handleGetProperties(
- $propFind,
- $node
- );
-
- $result = $propFind->getResultForMultiStatus();
-
- $this->assertEmpty($result[404]);
- unset($result[404]);
- $this->assertEquals($expectedProperties, $result);
- }
-
- public function testUpdateProperties() {
- $systemTag = new SystemTag(1, 'Test', true, false);
- $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagNode')
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->any())
- ->method('getSystemTag')
- ->will($this->returnValue($systemTag));
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/systemtag/1')
- ->will($this->returnValue($node));
-
- $node->expects($this->once())
- ->method('update')
- ->with('Test changed', false, true);
-
- // properties to set
- $propPatch = new \Sabre\DAV\PropPatch(array(
- self::DISPLAYNAME_PROPERTYNAME => 'Test changed',
- self::USERVISIBLE_PROPERTYNAME => 'false',
- self::USERASSIGNABLE_PROPERTYNAME => 'true',
- ));
-
- $this->plugin->handleUpdateProperties(
- '/systemtag/1',
- $propPatch
- );
-
- $propPatch->commit();
-
- // all requested properties removed, as they were processed already
- $this->assertEmpty($propPatch->getRemainingMutations());
-
- $result = $propPatch->getResult();
- $this->assertEquals(200, $result[self::DISPLAYNAME_PROPERTYNAME]);
- $this->assertEquals(200, $result[self::USERASSIGNABLE_PROPERTYNAME]);
- $this->assertEquals(200, $result[self::USERVISIBLE_PROPERTYNAME]);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\BadRequest
- * @expectedExceptionMessage Not sufficient permissions
- */
- public function testCreateNotAssignableTagAsRegularUser() {
- $user = $this->getMock('\OCP\IUser');
- $user->expects($this->once())
- ->method('getUID')
- ->willReturn('admin');
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->willReturn(true);
- $this->userSession
- ->expects($this->once())
- ->method('getUser')
- ->willReturn($user);
- $this->groupManager
- ->expects($this->once())
- ->method('isAdmin')
- ->with('admin')
- ->willReturn(false);
-
- $requestData = json_encode([
- 'name' => 'Test',
- 'userVisible' => true,
- 'userAssignable' => false,
- ]);
-
- $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection')
- ->disableOriginalConstructor()
- ->getMock();
- $this->tagManager->expects($this->never())
- ->method('createTag');
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/systemtags')
- ->will($this->returnValue($node));
-
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
- $request->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue('/systemtags'));
-
- $request->expects($this->once())
- ->method('getBodyAsString')
- ->will($this->returnValue($requestData));
-
- $request->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue('application/json'));
-
- $this->plugin->httpPost($request, $response);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\BadRequest
- * @expectedExceptionMessage Not sufficient permissions
- */
- public function testCreateInvisibleTagAsRegularUser() {
- $user = $this->getMock('\OCP\IUser');
- $user->expects($this->once())
- ->method('getUID')
- ->willReturn('admin');
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->willReturn(true);
- $this->userSession
- ->expects($this->once())
- ->method('getUser')
- ->willReturn($user);
- $this->groupManager
- ->expects($this->once())
- ->method('isAdmin')
- ->with('admin')
- ->willReturn(false);
-
- $requestData = json_encode([
- 'name' => 'Test',
- 'userVisible' => false,
- 'userAssignable' => true,
- ]);
-
- $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection')
- ->disableOriginalConstructor()
- ->getMock();
- $this->tagManager->expects($this->never())
- ->method('createTag');
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/systemtags')
- ->will($this->returnValue($node));
-
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
- $request->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue('/systemtags'));
-
- $request->expects($this->once())
- ->method('getBodyAsString')
- ->will($this->returnValue($requestData));
-
- $request->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue('application/json'));
-
- $this->plugin->httpPost($request, $response);
- }
-
- public function testCreateTagInByIdCollectionAsRegularUser() {
- $systemTag = new SystemTag(1, 'Test', true, false);
-
- $requestData = json_encode([
- 'name' => 'Test',
- 'userVisible' => true,
- 'userAssignable' => true,
- ]);
-
- $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection')
- ->disableOriginalConstructor()
- ->getMock();
- $this->tagManager->expects($this->once())
- ->method('createTag')
- ->with('Test', true, true)
- ->will($this->returnValue($systemTag));
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/systemtags')
- ->will($this->returnValue($node));
-
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
- $request->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue('/systemtags'));
-
- $request->expects($this->once())
- ->method('getBodyAsString')
- ->will($this->returnValue($requestData));
-
- $request->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue('application/json'));
-
- $request->expects($this->once())
- ->method('getUrl')
- ->will($this->returnValue('http://example.com/dav/systemtags'));
-
- $response->expects($this->once())
- ->method('setHeader')
- ->with('Content-Location', 'http://example.com/dav/systemtags/1');
-
- $this->plugin->httpPost($request, $response);
- }
-
- public function testCreateTagInByIdCollection() {
- $user = $this->getMock('\OCP\IUser');
- $user->expects($this->once())
- ->method('getUID')
- ->willReturn('admin');
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->willReturn(true);
- $this->userSession
- ->expects($this->once())
- ->method('getUser')
- ->willReturn($user);
- $this->groupManager
- ->expects($this->once())
- ->method('isAdmin')
- ->with('admin')
- ->willReturn(true);
-
- $systemTag = new SystemTag(1, 'Test', true, false);
-
- $requestData = json_encode([
- 'name' => 'Test',
- 'userVisible' => true,
- 'userAssignable' => false,
- ]);
-
- $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection')
- ->disableOriginalConstructor()
- ->getMock();
- $this->tagManager->expects($this->once())
- ->method('createTag')
- ->with('Test', true, false)
- ->will($this->returnValue($systemTag));
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/systemtags')
- ->will($this->returnValue($node));
-
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
- $request->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue('/systemtags'));
-
- $request->expects($this->once())
- ->method('getBodyAsString')
- ->will($this->returnValue($requestData));
-
- $request->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue('application/json'));
-
- $request->expects($this->once())
- ->method('getUrl')
- ->will($this->returnValue('http://example.com/dav/systemtags'));
-
- $response->expects($this->once())
- ->method('setHeader')
- ->with('Content-Location', 'http://example.com/dav/systemtags/1');
-
- $this->plugin->httpPost($request, $response);
- }
-
- public function nodeClassProvider() {
- return [
- ['\OCA\DAV\SystemTag\SystemTagsByIdCollection'],
- ['\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection'],
- ];
- }
-
- public function testCreateTagInMappingCollection() {
- $user = $this->getMock('\OCP\IUser');
- $user->expects($this->once())
- ->method('getUID')
- ->willReturn('admin');
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->willReturn(true);
- $this->userSession
- ->expects($this->once())
- ->method('getUser')
- ->willReturn($user);
- $this->groupManager
- ->expects($this->once())
- ->method('isAdmin')
- ->with('admin')
- ->willReturn(true);
-
- $systemTag = new SystemTag(1, 'Test', true, false);
-
- $requestData = json_encode([
- 'name' => 'Test',
- 'userVisible' => true,
- 'userAssignable' => false,
- ]);
-
- $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->tagManager->expects($this->once())
- ->method('createTag')
- ->with('Test', true, false)
- ->will($this->returnValue($systemTag));
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/systemtags-relations/files/12')
- ->will($this->returnValue($node));
-
- $node->expects($this->once())
- ->method('createFile')
- ->with(1);
-
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
- $request->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue('/systemtags-relations/files/12'));
-
- $request->expects($this->once())
- ->method('getBodyAsString')
- ->will($this->returnValue($requestData));
-
- $request->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue('application/json'));
-
- $request->expects($this->once())
- ->method('getBaseUrl')
- ->will($this->returnValue('http://example.com/dav/'));
-
- $response->expects($this->once())
- ->method('setHeader')
- ->with('Content-Location', 'http://example.com/dav/systemtags/1');
-
- $this->plugin->httpPost($request, $response);
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotFound
- */
- public function testCreateTagToUnknownNode() {
- $systemTag = new SystemTag(1, 'Test', true, false);
-
- $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->will($this->throwException(new \Sabre\DAV\Exception\NotFound()));
-
- $this->tagManager->expects($this->never())
- ->method('createTag');
-
- $node->expects($this->never())
- ->method('createFile');
-
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
- $request->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue('/systemtags-relations/files/12'));
-
- $this->plugin->httpPost($request, $response);
- }
-
- /**
- * @dataProvider nodeClassProvider
- * @expectedException \Sabre\DAV\Exception\Conflict
- */
- public function testCreateTagConflict($nodeClass) {
- $user = $this->getMock('\OCP\IUser');
- $user->expects($this->once())
- ->method('getUID')
- ->willReturn('admin');
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->willReturn(true);
- $this->userSession
- ->expects($this->once())
- ->method('getUser')
- ->willReturn($user);
- $this->groupManager
- ->expects($this->once())
- ->method('isAdmin')
- ->with('admin')
- ->willReturn(true);
-
- $requestData = json_encode([
- 'name' => 'Test',
- 'userVisible' => true,
- 'userAssignable' => false,
- ]);
-
- $node = $this->getMockBuilder($nodeClass)
- ->disableOriginalConstructor()
- ->getMock();
- $this->tagManager->expects($this->once())
- ->method('createTag')
- ->with('Test', true, false)
- ->will($this->throwException(new TagAlreadyExistsException('Tag already exists')));
-
- $this->tree->expects($this->any())
- ->method('getNodeForPath')
- ->with('/systemtags')
- ->will($this->returnValue($node));
-
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
- $request->expects($this->once())
- ->method('getPath')
- ->will($this->returnValue('/systemtags'));
-
- $request->expects($this->once())
- ->method('getBodyAsString')
- ->will($this->returnValue($requestData));
-
- $request->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->will($this->returnValue('application/json'));
-
- $this->plugin->httpPost($request, $response);
- }
-
-}
diff --git a/apps/dav/tests/unit/systemtag/systemtagsbyidcollection.php b/apps/dav/tests/unit/systemtag/systemtagsbyidcollection.php
deleted file mode 100644
index a2bf571ab68..00000000000
--- a/apps/dav/tests/unit/systemtag/systemtagsbyidcollection.php
+++ /dev/null
@@ -1,244 +0,0 @@
-<?php
-/**
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\SystemTag;
-
-
-use OC\SystemTag\SystemTag;
-use OCP\SystemTag\TagNotFoundException;
-
-class SystemTagsByIdCollection extends \Test\TestCase {
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- protected function setUp() {
- parent::setUp();
-
- $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager');
- }
-
- public function getNode($isAdmin = true) {
- $user = $this->getMock('\OCP\IUser');
- $user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('testuser'));
- $userSession = $this->getMock('\OCP\IUserSession');
- $userSession->expects($this->any())
- ->method('getUser')
- ->will($this->returnValue($user));
- $groupManager = $this->getMock('\OCP\IGroupManager');
- $groupManager->expects($this->any())
- ->method('isAdmin')
- ->with('testuser')
- ->will($this->returnValue($isAdmin));
- return new \OCA\DAV\SystemTag\SystemTagsByIdCollection(
- $this->tagManager,
- $userSession,
- $groupManager
- );
- }
-
- public function adminFlagProvider() {
- return [[true], [false]];
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\Forbidden
- */
- public function testForbiddenCreateFile() {
- $this->getNode()->createFile('555');
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\Forbidden
- */
- public function testForbiddenCreateDirectory() {
- $this->getNode()->createDirectory('789');
- }
-
- public function getChildProvider() {
- return [
- [
- true,
- true,
- ],
- [
- true,
- false,
- ],
- [
- false,
- true,
- ],
- ];
- }
-
- /**
- * @dataProvider getChildProvider
- */
- public function testGetChild($isAdmin, $userVisible) {
- $tag = new SystemTag(123, 'Test', $userVisible, false);
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['123'])
- ->will($this->returnValue([$tag]));
-
- $childNode = $this->getNode($isAdmin)->getChild('123');
-
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $childNode);
- $this->assertEquals('123', $childNode->getName());
- $this->assertEquals($tag, $childNode->getSystemTag());
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\BadRequest
- */
- public function testGetChildInvalidName() {
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['invalid'])
- ->will($this->throwException(new \InvalidArgumentException()));
-
- $this->getNode()->getChild('invalid');
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\NotFound
- */
- public function testGetChildNotFound() {
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['444'])
- ->will($this->throwException(new TagNotFoundException()));
-
- $this->getNode()->getChild('444');
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\NotFound
- */
- public function testGetChildUserNotVisible() {
- $tag = new SystemTag(123, 'Test', false, false);
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['123'])
- ->will($this->returnValue([$tag]));
-
- $this->getNode(false)->getChild('123');
- }
-
- public function testGetChildrenAdmin() {
- $tag1 = new SystemTag(123, 'One', true, false);
- $tag2 = new SystemTag(456, 'Two', true, true);
-
- $this->tagManager->expects($this->once())
- ->method('getAllTags')
- ->with(null)
- ->will($this->returnValue([$tag1, $tag2]));
-
- $children = $this->getNode(true)->getChildren();
-
- $this->assertCount(2, $children);
-
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[0]);
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[1]);
- $this->assertEquals($tag1, $children[0]->getSystemTag());
- $this->assertEquals($tag2, $children[1]->getSystemTag());
- }
-
- public function testGetChildrenNonAdmin() {
- $tag1 = new SystemTag(123, 'One', true, false);
- $tag2 = new SystemTag(456, 'Two', true, true);
-
- $this->tagManager->expects($this->once())
- ->method('getAllTags')
- ->with(true)
- ->will($this->returnValue([$tag1, $tag2]));
-
- $children = $this->getNode(false)->getChildren();
-
- $this->assertCount(2, $children);
-
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[0]);
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[1]);
- $this->assertEquals($tag1, $children[0]->getSystemTag());
- $this->assertEquals($tag2, $children[1]->getSystemTag());
- }
-
- public function testGetChildrenEmpty() {
- $this->tagManager->expects($this->once())
- ->method('getAllTags')
- ->with(null)
- ->will($this->returnValue([]));
- $this->assertCount(0, $this->getNode()->getChildren());
- }
-
- public function childExistsProvider() {
- return [
- // admins, always visible
- [true, true, true],
- [true, false, true],
- // non-admins, depends on flag
- [false, true, true],
- [false, false, false],
- ];
- }
-
- /**
- * @dataProvider childExistsProvider
- */
- public function testChildExists($isAdmin, $userVisible, $expectedResult) {
- $tag = new SystemTag(123, 'One', $userVisible, false);
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['123'])
- ->will($this->returnValue([$tag]));
-
- $this->assertEquals($expectedResult, $this->getNode($isAdmin)->childExists('123'));
- }
-
- public function testChildExistsNotFound() {
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['123'])
- ->will($this->throwException(new TagNotFoundException()));
-
- $this->assertFalse($this->getNode()->childExists('123'));
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\BadRequest
- */
- public function testChildExistsBadRequest() {
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['invalid'])
- ->will($this->throwException(new \InvalidArgumentException()));
-
- $this->getNode()->childExists('invalid');
- }
-}
diff --git a/apps/dav/tests/unit/systemtag/systemtagsobjectmappingcollection.php b/apps/dav/tests/unit/systemtag/systemtagsobjectmappingcollection.php
deleted file mode 100644
index df97acd846b..00000000000
--- a/apps/dav/tests/unit/systemtag/systemtagsobjectmappingcollection.php
+++ /dev/null
@@ -1,381 +0,0 @@
-<?php
-/**
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\SystemTag;
-
-
-use OC\SystemTag\SystemTag;
-use OCP\SystemTag\TagNotFoundException;
-
-class SystemTagsObjectMappingCollection extends \Test\TestCase {
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\SystemTag\ISystemTagMapper
- */
- private $tagMapper;
-
- protected function setUp() {
- parent::setUp();
-
- $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager');
- $this->tagMapper = $this->getMock('\OCP\SystemTag\ISystemTagObjectMapper');
- }
-
- public function getNode($isAdmin = true) {
- return new \OCA\DAV\SystemTag\SystemTagsObjectMappingCollection (
- 111,
- 'files',
- $isAdmin,
- $this->tagManager,
- $this->tagMapper
- );
- }
-
- public function testAssignTagAsAdmin() {
- $this->tagManager->expects($this->never())
- ->method('getTagsByIds');
- $this->tagMapper->expects($this->once())
- ->method('assignTags')
- ->with(111, 'files', '555');
-
- $this->getNode(true)->createFile('555');
- }
-
- public function testAssignTagAsUser() {
- $tag = new SystemTag('1', 'Test', true, true);
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with('555')
- ->will($this->returnValue([$tag]));
- $this->tagMapper->expects($this->once())
- ->method('assignTags')
- ->with(111, 'files', '555');
-
- $this->getNode(false)->createFile('555');
- }
-
- public function permissionsProvider() {
- return [
- // invisible, tag does not exist for user
- [false, true, '\Sabre\DAV\Exception\PreconditionFailed'],
- // visible but static, cannot assign tag
- [true, false, '\Sabre\DAV\Exception\Forbidden'],
- ];
- }
-
- /**
- * @dataProvider permissionsProvider
- */
- public function testAssignTagAsUserNoPermission($userVisible, $userAssignable, $expectedException) {
- $tag = new SystemTag('1', 'Test', $userVisible, $userAssignable);
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with('555')
- ->will($this->returnValue([$tag]));
- $this->tagMapper->expects($this->never())
- ->method('assignTags');
-
- $thrown = null;
- try {
- $this->getNode(false)->createFile('555');
- } catch (\Exception $e) {
- $thrown = $e;
- }
-
- $this->assertInstanceOf($expectedException, $thrown);
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\PreconditionFailed
- */
- public function testAssignTagNotFound() {
- $this->tagMapper->expects($this->once())
- ->method('assignTags')
- ->with(111, 'files', '555')
- ->will($this->throwException(new TagNotFoundException()));
-
- $this->getNode()->createFile('555');
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\Forbidden
- */
- public function testForbiddenCreateDirectory() {
- $this->getNode()->createDirectory('789');
- }
-
- public function getChildProvider() {
- return [
- [
- true,
- true,
- ],
- [
- true,
- false,
- ],
- [
- false,
- true,
- ],
- ];
- }
-
- /**
- * @dataProvider getChildProvider
- */
- public function testGetChild($isAdmin, $userVisible) {
- $tag = new SystemTag(555, 'TheTag', $userVisible, false);
-
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', '555', true)
- ->will($this->returnValue(true));
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['555'])
- ->will($this->returnValue(['555' => $tag]));
-
- $childNode = $this->getNode($isAdmin)->getChild('555');
-
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $childNode);
- $this->assertEquals('555', $childNode->getName());
- }
-
- /**
- * @expectedException \Sabre\DAV\Exception\NotFound
- */
- public function testGetChildUserNonVisible() {
- $tag = new SystemTag(555, 'TheTag', false, false);
-
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', '555', true)
- ->will($this->returnValue(true));
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['555'])
- ->will($this->returnValue(['555' => $tag]));
-
- $this->getNode(false)->getChild('555');
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\NotFound
- */
- public function testGetChildRelationNotFound() {
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', '777')
- ->will($this->returnValue(false));
-
- $this->getNode()->getChild('777');
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\BadRequest
- */
- public function testGetChildInvalidId() {
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', 'badid')
- ->will($this->throwException(new \InvalidArgumentException()));
-
- $this->getNode()->getChild('badid');
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\NotFound
- */
- public function testGetChildTagDoesNotExist() {
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', '777')
- ->will($this->throwException(new TagNotFoundException()));
-
- $this->getNode()->getChild('777');
- }
-
- public function testGetChildrenAsAdmin() {
- $tag1 = new SystemTag(555, 'TagOne', true, false);
- $tag2 = new SystemTag(556, 'TagTwo', true, true);
- $tag3 = new SystemTag(557, 'InvisibleTag', false, true);
-
- $this->tagMapper->expects($this->once())
- ->method('getTagIdsForObjects')
- ->with([111], 'files')
- ->will($this->returnValue(['111' => ['555', '556', '557']]));
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['555', '556', '557'])
- ->will($this->returnValue(['555' => $tag1, '556' => $tag2, '557' => $tag3]));
-
- $children = $this->getNode(true)->getChildren();
-
- $this->assertCount(3, $children);
-
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[0]);
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[1]);
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[2]);
-
- $this->assertEquals(111, $children[0]->getObjectId());
- $this->assertEquals('files', $children[0]->getObjectType());
- $this->assertEquals($tag1, $children[0]->getSystemTag());
-
- $this->assertEquals(111, $children[1]->getObjectId());
- $this->assertEquals('files', $children[1]->getObjectType());
- $this->assertEquals($tag2, $children[1]->getSystemTag());
-
- $this->assertEquals(111, $children[2]->getObjectId());
- $this->assertEquals('files', $children[2]->getObjectType());
- $this->assertEquals($tag3, $children[2]->getSystemTag());
- }
-
- public function testGetChildrenAsUser() {
- $tag1 = new SystemTag(555, 'TagOne', true, false);
- $tag2 = new SystemTag(556, 'TagTwo', true, true);
- $tag3 = new SystemTag(557, 'InvisibleTag', false, true);
-
- $this->tagMapper->expects($this->once())
- ->method('getTagIdsForObjects')
- ->with([111], 'files')
- ->will($this->returnValue(['111' => ['555', '556', '557']]));
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with(['555', '556', '557'])
- ->will($this->returnValue(['555' => $tag1, '556' => $tag2, '557' => $tag3]));
-
- $children = $this->getNode(false)->getChildren();
-
- $this->assertCount(2, $children);
-
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[0]);
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[1]);
-
- $this->assertEquals(111, $children[0]->getObjectId());
- $this->assertEquals('files', $children[0]->getObjectType());
- $this->assertEquals($tag1, $children[0]->getSystemTag());
-
- $this->assertEquals(111, $children[1]->getObjectId());
- $this->assertEquals('files', $children[1]->getObjectType());
- $this->assertEquals($tag2, $children[1]->getSystemTag());
- }
-
- public function testChildExistsAsAdmin() {
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', '555')
- ->will($this->returnValue(true));
-
- $this->assertTrue($this->getNode(true)->childExists('555'));
- }
-
- public function testChildExistsWithVisibleTagAsUser() {
- $tag = new SystemTag(555, 'TagOne', true, false);
-
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', '555')
- ->will($this->returnValue(true));
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with('555')
- ->will($this->returnValue([$tag]));
-
- $this->assertTrue($this->getNode(false)->childExists('555'));
- }
-
- public function testChildExistsWithInvisibleTagAsUser() {
- $tag = new SystemTag(555, 'TagOne', false, false);
-
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', '555')
- ->will($this->returnValue(true));
-
- $this->tagManager->expects($this->once())
- ->method('getTagsByIds')
- ->with('555')
- ->will($this->returnValue([$tag]));
-
- $this->assertFalse($this->getNode(false)->childExists('555'));
- }
-
- public function testChildExistsNotFound() {
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', '555')
- ->will($this->returnValue(false));
-
- $this->assertFalse($this->getNode()->childExists('555'));
- }
-
- public function testChildExistsTagNotFound() {
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', '555')
- ->will($this->throwException(new TagNotFoundException()));
-
- $this->assertFalse($this->getNode()->childExists('555'));
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\BadRequest
- */
- public function testChildExistsInvalidId() {
- $this->tagMapper->expects($this->once())
- ->method('haveTag')
- ->with([111], 'files', '555')
- ->will($this->throwException(new \InvalidArgumentException()));
-
- $this->getNode()->childExists('555');
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\Forbidden
- */
- public function testDelete() {
- $this->getNode()->delete();
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\Forbidden
- */
- public function testSetName() {
- $this->getNode()->setName('somethingelse');
- }
-
- public function testGetName() {
- $this->assertEquals('111', $this->getNode()->getName());
- }
-}
diff --git a/apps/dav/tests/unit/systemtag/systemtagsobjecttypecollection.php b/apps/dav/tests/unit/systemtag/systemtagsobjecttypecollection.php
deleted file mode 100644
index 1d4264f94f9..00000000000
--- a/apps/dav/tests/unit/systemtag/systemtagsobjecttypecollection.php
+++ /dev/null
@@ -1,160 +0,0 @@
-<?php
-/**
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\DAV\Tests\Unit\SystemTag;
-
-class SystemTagsObjectTypeCollection extends \Test\TestCase {
-
- /**
- * @var \OCA\DAV\SystemTag\SystemTagsObjectTypeCollection
- */
- private $node;
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\SystemTag\ISystemTagMapper
- */
- private $tagMapper;
-
- /**
- * @var \OCP\Files\Folder
- */
- private $userFolder;
-
- protected function setUp() {
- parent::setUp();
-
- $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager');
- $this->tagMapper = $this->getMock('\OCP\SystemTag\ISystemTagObjectMapper');
-
- $user = $this->getMock('\OCP\IUser');
- $user->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('testuser'));
- $userSession = $this->getMock('\OCP\IUserSession');
- $userSession->expects($this->any())
- ->method('getUser')
- ->will($this->returnValue($user));
- $groupManager = $this->getMock('\OCP\IGroupManager');
- $groupManager->expects($this->any())
- ->method('isAdmin')
- ->with('testuser')
- ->will($this->returnValue(true));
-
- $this->userFolder = $this->getMock('\OCP\Files\Folder');
-
- $fileRoot = $this->getMock('\OCP\Files\IRootFolder');
- $fileRoot->expects($this->any())
- ->method('getUserfolder')
- ->with('testuser')
- ->will($this->returnValue($this->userFolder));
-
- $this->node = new \OCA\DAV\SystemTag\SystemTagsObjectTypeCollection(
- 'files',
- $this->tagManager,
- $this->tagMapper,
- $userSession,
- $groupManager,
- $fileRoot
- );
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\Forbidden
- */
- public function testForbiddenCreateFile() {
- $this->node->createFile('555');
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\Forbidden
- */
- public function testForbiddenCreateDirectory() {
- $this->node->createDirectory('789');
- }
-
- public function testGetChild() {
- $this->userFolder->expects($this->once())
- ->method('getById')
- ->with('555')
- ->will($this->returnValue([true]));
- $childNode = $this->node->getChild('555');
-
- $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection', $childNode);
- $this->assertEquals('555', $childNode->getName());
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\NotFound
- */
- public function testGetChildWithoutAccess() {
- $this->userFolder->expects($this->once())
- ->method('getById')
- ->with('555')
- ->will($this->returnValue([]));
- $this->node->getChild('555');
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\MethodNotAllowed
- */
- public function testGetChildren() {
- $this->node->getChildren();
- }
-
- public function testChildExists() {
- $this->userFolder->expects($this->once())
- ->method('getById')
- ->with('123')
- ->will($this->returnValue([true]));
- $this->assertTrue($this->node->childExists('123'));
- }
-
- public function testChildExistsWithoutAccess() {
- $this->userFolder->expects($this->once())
- ->method('getById')
- ->with('555')
- ->will($this->returnValue([]));
- $this->assertFalse($this->node->childExists('555'));
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\Forbidden
- */
- public function testDelete() {
- $this->node->delete();
- }
-
- /**
- * @expectedException Sabre\DAV\Exception\Forbidden
- */
- public function testSetName() {
- $this->node->setName('somethingelse');
- }
-
- public function testGetName() {
- $this->assertEquals('files', $this->node->getName());
- }
-}
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics
new file mode 100644
index 00000000000..e76ac3c9b2f
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics
@@ -0,0 +1,17 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T105946Z
+LAST-MODIFIED:20240507T121113Z
+DTSTAMP:20240507T121113Z
+UID:07514c7b-1014-425c-b1b8-2c35ab0eea1d
+SUMMARY:Event A
+RRULE:FREQ=YEARLY
+DTSTART;TZID=Europe/Berlin:20240101T101500
+DTEND;TZID=Europe/Berlin:20240101T111500
+TRANSP:OPAQUE
+X-MOZ-GENERATION:4
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics
new file mode 100644
index 00000000000..fe948321d51
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics
@@ -0,0 +1,17 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T110122Z
+LAST-MODIFIED:20240507T121120Z
+DTSTAMP:20240507T121120Z
+UID:67cf8134-ff10-49a7-913d-acfeda463db6
+SUMMARY:Event B
+RRULE:FREQ=YEARLY
+DTSTART;TZID=Europe/Berlin:20240101T123000
+DTEND;TZID=Europe/Berlin:20240101T133000
+TRANSP:OPAQUE
+X-MOZ-GENERATION:4
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics
new file mode 100644
index 00000000000..de7765b28d2
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics
@@ -0,0 +1,17 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T120352Z
+LAST-MODIFIED:20240507T121128Z
+DTSTAMP:20240507T121128Z
+UID:59090ca1-e52b-447f-8e08-491d1da729fa
+SUMMARY:Event C
+RRULE:FREQ=YEARLY
+DTSTART;TZID=Europe/Berlin:20240101T151000
+DTEND;TZID=Europe/Berlin:20240101T161000
+TRANSP:OPAQUE
+X-MOZ-GENERATION:2
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics
new file mode 100644
index 00000000000..b4d2f752c0a
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics
@@ -0,0 +1,17 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T120414Z
+LAST-MODIFIED:20240507T121134Z
+DTSTAMP:20240507T121134Z
+UID:b1814d32-9adf-4518-8535-37f2c037f423
+SUMMARY:Event D
+RRULE:FREQ=YEARLY
+DTSTART;TZID=Europe/Berlin:20240101T164500
+DTEND;TZID=Europe/Berlin:20240101T171500
+TRANSP:OPAQUE
+SEQUENCE:2
+X-MOZ-GENERATION:3
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics
new file mode 100644
index 00000000000..1cd8b7ebf13
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics
@@ -0,0 +1,14 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T122221Z
+LAST-MODIFIED:20240507T122237Z
+DTSTAMP:20240507T122237Z
+UID:19c4e049-0b09-4101-a2ad-061a837e6a5e
+SUMMARY:Cake Tasting
+DTSTART;TZID=Europe/Berlin:20240509T151500
+DTEND;TZID=Europe/Berlin:20240509T171500
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics
new file mode 100644
index 00000000000..6c24d534281
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics
@@ -0,0 +1,15 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T122246Z
+LAST-MODIFIED:20240507T175258Z
+DTSTAMP:20240507T175258Z
+UID:60a7d310-aa7b-4974-8a8a-ff9339367e1d
+SUMMARY:Pasta Day
+DTSTART;TZID=Europe/Berlin:20240514T123000
+DTEND;TZID=Europe/Berlin:20240514T133000
+TRANSP:OPAQUE
+X-MOZ-GENERATION:2
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics
new file mode 100644
index 00000000000..a7865eaf5ef
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics
@@ -0,0 +1,14 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T122246Z
+LAST-MODIFIED:20240507T175258Z
+DTSTAMP:20240507T175258Z
+UID:39e1b04f-d1cc-4622-bf97-11c38e070f43
+SUMMARY:Missing DTSTART 1
+DTEND;TZID=Europe/Berlin:20240514T133000
+TRANSP:OPAQUE
+X-MOZ-GENERATION:2
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics
new file mode 100644
index 00000000000..4a33f2b1c8a
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics
@@ -0,0 +1,14 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T122246Z
+LAST-MODIFIED:20240507T175258Z
+DTSTAMP:20240507T175258Z
+UID:12413feb-4b8c-4e95-ae7f-9ec4f42f3348
+SUMMARY:Missing DTSTART 2
+DTEND;TZID=Europe/Berlin:20240514T133000
+TRANSP:OPAQUE
+X-MOZ-GENERATION:2
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics
new file mode 100644
index 00000000000..09606ca5ee4
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics
@@ -0,0 +1,20 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 4.5.6//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:RANDOM-UID
+DTSTAMP:20250121T000000Z
+SUMMARY:Example event - open me!
+DTSTART:20250128T100000Z
+DTEND:20250128T110000Z
+DESCRIPTION:Welcome to Nextcloud Calendar!\n\nThis is a sample event - expl
+ ore the flexibility of planning with Nextcloud Calendar by making any edit
+ s you want!\n\nWith Nextcloud Calendar\, you can:\n- Create\, edit\, and m
+ anage events effortlessly.\n- Create multiple calendars and share them wit
+ h teammates\, friends\, or family.\n- Check availability and display your
+ busy times to others.\n- Seamlessly integrate with apps and devices via Ca
+ lDAV.\n- Customize your experience: schedule recurring events\, adjust not
+ ifications and other settings.
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license
new file mode 100644
index 00000000000..23e2d6b1908
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-expected.ics b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics
new file mode 100644
index 00000000000..f9dfc37718e
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics
@@ -0,0 +1,18 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
+BEGIN:VEVENT
+CREATED:20250128T091147Z
+DTSTAMP:20250128T091507Z
+LAST-MODIFIED:20250128T091507Z
+SEQUENCE:2
+STATUS:CONFIRMED
+SUMMARY:Welcome!
+DESCRIPTION:Welcome!!!
+LOCATION:Test
+UID:RANDOM-UID
+DTSTART:20250128T100000Z
+DTEND:20250128T110000Z
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license
new file mode 100644
index 00000000000..23e2d6b1908
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics
new file mode 100644
index 00000000000..8018552f2a5
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
+BEGIN:VEVENT
+CREATED:20250128T091147Z
+DTSTAMP:20250128T091507Z
+LAST-MODIFIED:20250128T091507Z
+SEQUENCE:2
+UID:3b4df6a8-84df-43d5-baf9-377b43390b70
+DTSTART;VALUE=DATE:20250130
+DTEND;VALUE=DATE:20250131
+STATUS:CONFIRMED
+SUMMARY:Welcome!
+DESCRIPTION:Welcome!!!
+LOCATION:Test
+ATTENDEE;CN=user a;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICI
+ PANT;RSVP=TRUE;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:usera@imap.localhost
+ORGANIZER;CN=Admin Account:mailto:admin@imap.localhost
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license
new file mode 100644
index 00000000000..23e2d6b1908
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/tests/unit/test_fixtures/example-event.ics b/apps/dav/tests/unit/test_fixtures/example-event.ics
new file mode 100644
index 00000000000..6fc1848ea52
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event.ics
@@ -0,0 +1,18 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
+BEGIN:VEVENT
+CREATED:20250128T091147Z
+DTSTAMP:20250128T091507Z
+LAST-MODIFIED:20250128T091507Z
+SEQUENCE:2
+UID:3b4df6a8-84df-43d5-baf9-377b43390b70
+STATUS:CONFIRMED
+SUMMARY:Welcome!
+DESCRIPTION:Welcome!!!
+LOCATION:Test
+DTSTART:20250204T100000Z
+DTEND:20250204T110000Z
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event.ics.license b/apps/dav/tests/unit/test_fixtures/example-event.ics.license
new file mode 100644
index 00000000000..23e2d6b1908
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event.ics.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later