From aca5c5e79262c286e66fa1bcbdf2ae82dff334df Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 19 Feb 2022 17:06:51 +0100 Subject: [PATCH] patches sent --- apps/user_ldap/appinfo/signature.json | 407 +++++ apps/user_ldap/l10n/.gitkeep | 0 apps/user_ldap/lib/Access.php | 6 + apps/user_ldap/lib/Configuration.php | 6 + apps/user_ldap/lib/Connection.php | 35 +- apps/user_ldap/lib/Group_LDAP.php | 159 +- apps/user_ldap/lib/Wizard.php | 2 +- apps/user_ldap/tests/.htaccess | 16 - apps/user_ldap/tests/AccessTest.php | 784 --------- apps/user_ldap/tests/ConfigurationTest.php | 142 -- apps/user_ldap/tests/ConnectionTest.php | 291 ---- apps/user_ldap/tests/GroupLDAPPluginTest.php | 249 --- apps/user_ldap/tests/Group_LDAPTest.php | 1383 ---------------- apps/user_ldap/tests/HelperTest.php | 117 -- .../Integration/AbstractIntegrationTest.php | 180 -- .../user_ldap/tests/Integration/Bootstrap.php | 26 - .../Integration/ExceptionOnLostConnection.php | 198 --- .../Lib/IntegrationTestAttributeDetection.php | 89 - .../IntegrationTestCountUsersByLoginName.php | 71 - .../IntegrationTestFetchUsersByLoginName.php | 86 - .../Integration/Lib/IntegrationTestPaging.php | 99 -- .../Lib/User/IntegrationTestUserAvatar.php | 168 -- .../Lib/User/IntegrationTestUserCleanUp.php | 105 -- .../User/IntegrationTestUserDisplayName.php | 113 -- .../tests/Integration/data/avatar-invalid.gif | Bin 48702 -> 0 bytes .../tests/Integration/data/avatar-valid.jpg | Bin 75950 -> 0 bytes apps/user_ldap/tests/Integration/readme.md | 60 - apps/user_ldap/tests/Integration/run-all.sh | 34 - apps/user_ldap/tests/Integration/run-test.sh | 17 - .../setup-scripts/createExplicitGroups.php | 73 - .../createExplicitGroupsDifferentOU.php | 73 - .../setup-scripts/createExplicitUsers.php | 76 - .../createUsersWithoutDisplayName.php | 61 - apps/user_ldap/tests/Jobs/CleanUpTest.php | 155 -- apps/user_ldap/tests/Jobs/SyncTest.php | 386 ----- apps/user_ldap/tests/LDAPGroupPluginDummy.php | 56 - apps/user_ldap/tests/LDAPProviderTest.php | 699 -------- apps/user_ldap/tests/LDAPTest.php | 96 -- apps/user_ldap/tests/LDAPUserPluginDummy.php | 60 - .../tests/Mapping/AbstractMappingTest.php | 303 ---- .../tests/Mapping/GroupMappingTest.php | 41 - .../tests/Mapping/UserMappingTest.php | 41 - .../tests/Migration/AbstractUUIDFixTest.php | 198 --- .../tests/Migration/UUIDFixGroupTest.php | 50 - .../tests/Migration/UUIDFixInsertTest.php | 197 --- .../tests/Migration/UUIDFixUserTest.php | 47 - apps/user_ldap/tests/Settings/AdminTest.php | 91 -- apps/user_ldap/tests/Settings/SectionTest.php | 79 - .../tests/User/DeletedUsersIndexTest.php | 122 -- apps/user_ldap/tests/User/ManagerTest.php | 254 --- apps/user_ldap/tests/User/OfflineUserTest.php | 95 -- apps/user_ldap/tests/User/UserTest.php | 1230 -------------- apps/user_ldap/tests/UserLDAPPluginTest.php | 312 ---- apps/user_ldap/tests/User_LDAPTest.php | 1453 ----------------- apps/user_ldap/tests/User_ProxyTest.php | 104 -- apps/user_ldap/tests/WizardTest.php | 451 ----- 56 files changed, 544 insertions(+), 11102 deletions(-) create mode 100644 apps/user_ldap/appinfo/signature.json delete mode 100644 apps/user_ldap/l10n/.gitkeep delete mode 100755 apps/user_ldap/tests/.htaccess delete mode 100644 apps/user_ldap/tests/AccessTest.php delete mode 100644 apps/user_ldap/tests/ConfigurationTest.php delete mode 100644 apps/user_ldap/tests/ConnectionTest.php delete mode 100644 apps/user_ldap/tests/GroupLDAPPluginTest.php delete mode 100644 apps/user_ldap/tests/Group_LDAPTest.php delete mode 100644 apps/user_ldap/tests/HelperTest.php delete mode 100644 apps/user_ldap/tests/Integration/AbstractIntegrationTest.php delete mode 100644 apps/user_ldap/tests/Integration/Bootstrap.php delete mode 100644 apps/user_ldap/tests/Integration/ExceptionOnLostConnection.php delete mode 100644 apps/user_ldap/tests/Integration/Lib/IntegrationTestAttributeDetection.php delete mode 100644 apps/user_ldap/tests/Integration/Lib/IntegrationTestCountUsersByLoginName.php delete mode 100644 apps/user_ldap/tests/Integration/Lib/IntegrationTestFetchUsersByLoginName.php delete mode 100644 apps/user_ldap/tests/Integration/Lib/IntegrationTestPaging.php delete mode 100644 apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserAvatar.php delete mode 100644 apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserCleanUp.php delete mode 100644 apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserDisplayName.php delete mode 100644 apps/user_ldap/tests/Integration/data/avatar-invalid.gif delete mode 100644 apps/user_ldap/tests/Integration/data/avatar-valid.jpg delete mode 100644 apps/user_ldap/tests/Integration/readme.md delete mode 100755 apps/user_ldap/tests/Integration/run-all.sh delete mode 100755 apps/user_ldap/tests/Integration/run-test.sh delete mode 100644 apps/user_ldap/tests/Integration/setup-scripts/createExplicitGroups.php delete mode 100644 apps/user_ldap/tests/Integration/setup-scripts/createExplicitGroupsDifferentOU.php delete mode 100644 apps/user_ldap/tests/Integration/setup-scripts/createExplicitUsers.php delete mode 100644 apps/user_ldap/tests/Integration/setup-scripts/createUsersWithoutDisplayName.php delete mode 100644 apps/user_ldap/tests/Jobs/CleanUpTest.php delete mode 100644 apps/user_ldap/tests/Jobs/SyncTest.php delete mode 100644 apps/user_ldap/tests/LDAPGroupPluginDummy.php delete mode 100644 apps/user_ldap/tests/LDAPProviderTest.php delete mode 100644 apps/user_ldap/tests/LDAPTest.php delete mode 100644 apps/user_ldap/tests/LDAPUserPluginDummy.php delete mode 100644 apps/user_ldap/tests/Mapping/AbstractMappingTest.php delete mode 100644 apps/user_ldap/tests/Mapping/GroupMappingTest.php delete mode 100644 apps/user_ldap/tests/Mapping/UserMappingTest.php delete mode 100644 apps/user_ldap/tests/Migration/AbstractUUIDFixTest.php delete mode 100644 apps/user_ldap/tests/Migration/UUIDFixGroupTest.php delete mode 100644 apps/user_ldap/tests/Migration/UUIDFixInsertTest.php delete mode 100644 apps/user_ldap/tests/Migration/UUIDFixUserTest.php delete mode 100644 apps/user_ldap/tests/Settings/AdminTest.php delete mode 100644 apps/user_ldap/tests/Settings/SectionTest.php delete mode 100644 apps/user_ldap/tests/User/DeletedUsersIndexTest.php delete mode 100644 apps/user_ldap/tests/User/ManagerTest.php delete mode 100644 apps/user_ldap/tests/User/OfflineUserTest.php delete mode 100644 apps/user_ldap/tests/User/UserTest.php delete mode 100644 apps/user_ldap/tests/UserLDAPPluginTest.php delete mode 100644 apps/user_ldap/tests/User_LDAPTest.php delete mode 100644 apps/user_ldap/tests/User_ProxyTest.php delete mode 100644 apps/user_ldap/tests/WizardTest.php diff --git a/apps/user_ldap/appinfo/signature.json b/apps/user_ldap/appinfo/signature.json new file mode 100644 index 0000000000000..3aa4827fc96fd --- /dev/null +++ b/apps/user_ldap/appinfo/signature.json @@ -0,0 +1,407 @@ +{ + "hashes": { + "ajax\/clearMappings.php": "b13727cfaf56e4f09325eb84981961144608e91bcc9fc433de91c2eea324cc414fd6f96958123f18b27ea169944999c6ecbd0be6aa1d669172494f8c979ace48", + "ajax\/deleteConfiguration.php": "f9ab4f01c6c731e3a4d6907f05233b113972da205442854ba9d2656fea6104682542bfa52c00b893de283f3a9b2db8a0db0b38730e383546ff5689903701231d", + "ajax\/getConfiguration.php": "58c8ddc9e654743d79a40ff3299f138af2e690afbc2d46f037a7258f414182101a80dd23997cc7a2ddec68eeb4f8453d36992386c4f3c11ad1cf7ec275565a8d", + "ajax\/getNewServerConfigPrefix.php": "8c1cac236e615fe118b0060f2aaa03f79335b47234bf633f9f54b9f707598a4c9db22b41a83eb3a69b2dd86b9b60a3ee12f2f0cb2b7b92b71968183cebf9afa0", + "ajax\/setConfiguration.php": "a9b42b86d15dfe19b98cf879ae7c99efb3528089a5b79cf1817cd75c62138b2861b710d9f471551a882a8e866ae01b75d9823ab43992823333bc2fda8dd80fbc", + "ajax\/testConfiguration.php": "0895b234eef60fe1441c813fb72d94e8df6edb329dfe01dfce2242ad879600c6345628d97715eb1ae156e035f5ea0006cc5c8c016ef30c2cece64218dbb24f01", + "ajax\/wizard.php": "243965b131dba7182c77c383ca3b92a553186ee47c02bad723b916571e8fe2dd9d2e77d0adb07e519826a0f0007bdc928ce2d2ae282e1e6df43357243becaabe", + "appinfo\/info.xml": "69ab57071c80d50828cfce32d38fa97eed10e85ad71d26b3a96e1558bd3340961a749f5abfd7a7ecdbc311d8dc42a42990d70268375b21d98451ae46c3014aae", + "appinfo\/install.php": "1e8702f937a03034f57003ef1204dc93e7442f31df1174748c9a387069614ca79ebde7cf766d80c10e808c7ce589bfd7ead02daf8f13ceefc98b7450ac395122", + "appinfo\/register_command.php": "34c593226f6d68f812e6276f47f8636a66a60cce06d04cbaa38a0bc3e0d9c82f99c48949daf02953be3dddd5a608bdad43ec0124cc36104f8c42cb6c8f137b58", + "appinfo\/routes.php": "a57a4168e9b349763b7e953b15ef3909aa453b6c06514f5d4089ed653bb6aa7eb8e2681c3acecf76336cc793108911db31f4136f45cfdb0cda0c13680355651e", + "appinfo\/update.php": "8ae9fff2a64e7340d4f3981177abc6e44f9a9299f3d663b95dea8fd6e09e75c32907d4e096bad9416ca44e5a4e4761fa78e4de4820342efd6c09e6e2a086432e", + "composer\/autoload.php": "76a3962dca96dee4ac0e1afbaf670dcdd9379e8cc9481f9c2195dec5d86f0c2b2c4105bcce7173db4c8c5e039870d420603f0eb34ccadbdd69e3b8b8574368bd", + "composer\/composer.json": "3df63a6d4ea53ac3fcd1ab4a33620587353739d45e9d891e169ba515d1f74f5d0f10fe3fad84c64ce894c61c71c6450294c857d8cd2a4ab05dbd3548e3ef37fc", + "composer\/composer\/ClassLoader.php": "e95f90be793a521568a3bbe14f724e47f58173b8ed8c7907f799932fd9ef483ac4b9e741b572a0867a1afdba0c96fe7eb919980df240e158313286e4979cc66a", + "composer\/composer\/LICENSE": "f3bb64009f41a425df5a9bbab53490f0eb9b74fa8d6aaa2f57efb928edc4ffff330260666edeaa04a91fed708c3663371cf01b284f3a08d6698aaef7a23f355a", + "composer\/composer\/autoload_classmap.php": "c6b7703804928f9b33c3f33a58d236fcb2a578d421e079e80dd2e275b602d14639d1efb0a850a2d419e3b86d1f66b14748d087b0ffa5270f920082306d04d776", + "composer\/composer\/autoload_namespaces.php": "dd8ee31d2faa9014fe567e1f954b5e7f450cf06feacbb96d4df663c59b1a36a0b7d26ad41b46354bd14f282fa7899298137208d1851099cd3ada285650efd90f", + "composer\/composer\/autoload_psr4.php": "9558aee131a3748c0fb48ec8c91b5145638f33866d128433940d5fce1d584b811508db062099eeb0336fd0bf89a8760118ec43a1cfedc468633261be59aa1cdf", + "composer\/composer\/autoload_real.php": "2f121adb87f9dbcf9ea26239267c84252d0ccd469b8534c162848dae9f0cec237a83da59888cbd3eab4fcea0ea82a5d916d0dfb0b9e8e2381fb55da235e56018", + "composer\/composer\/autoload_static.php": "8e7d23dbfc8cee70bda5550c26e4da624f3c86708b546df0fa062cae6cf79be21459090b55b919bf67465982c1f8a8ac8a37cd8b8f0f3a790afde155ba687c7c", + "css\/renewPassword.css": "94c76430e2dcfbf27614113dd7caecf0a213d695318eb267c8c20e6ff107ea261905d8ae926073a15524c419924dab5951fc3f11e188b4a0277a54f7d826b8a6", + "css\/settings.css": "e4597900a692c046083a6b41be705778f4f163b42a9009b4b70a44707a18151f855f072c98b9ddef5e514ddf3ec7fcf36a0a41c0b8548144376bdf238bb0ea1c", + "img\/app-dark.svg": "de427b31679703fe33d206a8d071f051699dd08bb97cc36b7e0788004a6ad6baca58b57261aa33f52e3cdec7d2e21fb6ec2a4d016ae65bee66de49da48fcece4", + "img\/app.svg": "5c8be35c5076766bfd5718a92e4d05ca37e79732ec12f760886af84c8923224d347b1f9d9d6c1e94c48ba90858b4f4d4538bdaeb768272a2ba281c5c77a244d9", + "img\/copy.png": "6303e225dde6a1ed8d14dd2d441f9cb7ef2efa06ba5e2e91070459b93132213d2104901270dda099230f45ff513731991b869dfc4ee02393b07111a1f641cbf9", + "img\/copy.svg": "0d18f4de308dffe78a728a9777339efdc4126f85c6d2c2384c13609745f765620f990e2734cfbd9cd7d522e0c9ca62d3c2975641ce69f17976499dc254c56d45", + "js\/renewPassword.js": "509ca8b3ca5824c23210595ddf2aa1d2f38bf9aa28475f5618d18e081b5ff298a03189cd19f1ddb6b7f814845592b4c9675100a56e8a73490075da3c8cf7d1fd", + "js\/wizard\/configModel.js": "e6bffe941992874a9898300eb043aa6bf8fb81845869ff4301a07553bf67590f3960be6dc2c63bcd071024a10c5ae8d7d9284b5db83374b9b3f486d2cb383760", + "js\/wizard\/controller.js": "831dc868fc1ed9cc890214c7d8a9537d8e20ccae56a1c2cc5a8177acfbf3aa569597f51ff72038adf2de917eb762f69a61b7e9d6bca2e135bf5c980cc23ac933", + "js\/wizard\/view.js": "440ef11cd1c8503499dc4d125f574d07e7851ba6ffefb3c90752ed3924fe77e6067e465192c8bb0903e46a73ee4d3cd812b430d5806eacdb9a1b40ab4cdd9a53", + "js\/wizard\/wizard.js": "80032cbe47052bee60f54fe48995c70e19d07b2c0164fc8cdc804201619fe159c1451aa8de3863eb25ce988a3aca4fed7a21dac2a9961f80a461c67f0d4643fb", + "js\/wizard\/wizardDetectorAvailableAttributes.js": "770526bdceda18fec8fe5f1b7b3ce871a1928264091864834b612afbeeb98d020e9f769ed5873c63709e41a50d2605055ff4e992770c8b2760bf1118a1c87de2", + "js\/wizard\/wizardDetectorBaseDN.js": "88e9c3fa81a98198f4865e9a5024e5b5b35540eb2fe9032e6d8e90e4bdaed06432be69e3087099db1301e09b0ef72e49dc28cdf7e979863282576ce3b61f006a", + "js\/wizard\/wizardDetectorClearGroupMappings.js": "b43b6bcf2f476206055a24f887247ae0e5615382fbc586223518ce8836fe899eb4fa5d3993c12b714a3adb8d4eeb7f77200dd4408344e0e629702c8b7fb7b910", + "js\/wizard\/wizardDetectorClearUserMappings.js": "7e49a02453d17edf4216ac4f8d1a20529db500fe64dee1aaf2e4502fe60de88510957212c2a8fe246e38ae27180d64c386358910ff2f3dc6b7486d82d66bc6cc", + "js\/wizard\/wizardDetectorEmailAttribute.js": "f6f3723aab647fa6a2f38a52a34a29ff1f89126698039b4090919486c60a69a6f0dd62fc7e5458d7a1991172f5b1a1afb5380b7e8ef09f293dad474f7684fa0c", + "js\/wizard\/wizardDetectorFeatureAbstract.js": "730b67df59068523c892f3148c55abc6408461b3c9b72976574c83866b11298368447cbc897d0237f4b5ce94052c1084d3cf2407d4843ada43af4799c206554e", + "js\/wizard\/wizardDetectorFilterGroup.js": "c2cee452b2a91a84d0fae2054648b5720ee4d863b7233190a3b6bb54f82499c0e2d7f08a134d467044920149b3c47f5a9e1dbca22292bdca0732c2199b53004f", + "js\/wizard\/wizardDetectorFilterLogin.js": "8b0c6b6b05bf2e4b4c94fbd5f5e75659c12a732ee0df51f3248660cdadbe630bd2e4cc54a03724c7befeb89e2c2b54fb2a3860d3fb338bc3f8f7bbcff4662a31", + "js\/wizard\/wizardDetectorFilterUser.js": "af7d0a80e34012152e6e7f9f0f0edbbdf36a364bc693800bd9791205d6c8fd809afac9cd5f8f965f96491c40bcb8e6a02162ad73b07a12b4809ddbb22cb06f4d", + "js\/wizard\/wizardDetectorGeneric.js": "cc4dc083ec3529c5316051be091367fd66a8e8b9499bcd491dc26f831bb8582219b7e9be6517e95a52872939855d118ed17c6dc376ed2c440ca5a91861c9a926", + "js\/wizard\/wizardDetectorGroupCount.js": "47fddf38c98ed7d165d034f9367370dea3b5b00f1c57c2bc57554a53a64c4010e80e813409c64409f04bbfacaa64d10a12ed2c686b06f9734fa4b754627cb221", + "js\/wizard\/wizardDetectorGroupObjectClasses.js": "b898927813e56fb6be073bd1b2307ce0d28d5ff96ec158ffc9b7b8c3ccc47e0be8dce2eb4d940dc1954eafb7af5c611a0fa0c456b1ea89ee755a56414b44538a", + "js\/wizard\/wizardDetectorGroupsForGroups.js": "af88cd6550dca7cb3aec7b15640de0e54d6b58ae9b6c3b9004951bc23ee57ecebedc93b2d423efc822a45aeea9b05290888565381e1e0c7969ade90d2ebbdc57", + "js\/wizard\/wizardDetectorGroupsForUsers.js": "848df2ea9ce6b2f5d5c9ea2ae488b5978fff6ad88e917a3a7b7de5c3ab6e58dd2c33cf2f945463df648ff7d4a9cc735520643a3dcc7c802a3340ae0f373bf722", + "js\/wizard\/wizardDetectorPort.js": "c59500df1898bace884ef067254efe4d62456687d75f2bd626c74545163845972d911c1a766aa980fd4c13be644b7f28ac0793c43cf6d5ed3d068c282c9f202d", + "js\/wizard\/wizardDetectorQueue.js": "55e826710e56d073fe68a3ec9865a2939384b78bf1e00e18291ee49ad5c9aa741bb12c7a24d59895d89be255b2a8c8a2b544403d9eaea5a2b14a12679dbdc587", + "js\/wizard\/wizardDetectorSimpleRequestAbstract.js": "7985a8dcae3d662477e30e4c4cd6b10768a0eadf2b29f0760c4e66658e0a44a30e18ec23b098b27d24c8d6f00ddf7558639886c4f82f1e2cd651a799992f69bc", + "js\/wizard\/wizardDetectorTestAbstract.js": "723ad0ca8d5c7279cf83d0cfa809cd9a141a5b015a2f23255bbca40e89c79aef8e5b7a4f4ba1fefa2155dd3f2815a6036be8dacc2a35082e91214252d713d22b", + "js\/wizard\/wizardDetectorTestBaseDN.js": "fdaad3843ce8767bfa9525359142c9e54636d629d45b9b7d082f69b791a3081a16e5c4939fe441bba2eef4b604c0072ac2319a090a82a7fa54362f829afb7a6f", + "js\/wizard\/wizardDetectorTestConfiguration.js": "27b50b7564e58478f1e7a1b1918db26231170bd6264b812ec6047c1e313d640914e3fad568d8aed2b2a01be317e9d315752d26f42b7d7662ac8526c288ded009", + "js\/wizard\/wizardDetectorTestLoginName.js": "002517d45aea4e7456ceac1c6add1f52b3a79df52af0ef0e1fffab84ce59f858d33334b33d7a4fda2b936883e8ac5986eca933b55e705f850e0ac06116a3420a", + "js\/wizard\/wizardDetectorUserCount.js": "ac37b9bd82046afca6211a9c8daa3cc5abe59010c9266ece0ef7b6343ed2ee6448b41669b2698f228f87d0abe41a6b9ac0a22a5bc279f9af8ba6de4aeea3a8fd", + "js\/wizard\/wizardDetectorUserDisplayNameAttribute.js": "3c5b50c881d61ae7f98a48bfa5ebd0de02435f3475f8c5dd5f3c2f8bf11ecb5a177efcf4759bd069ed896350cecf430b35730fa8163c1b3f39595021490e00b0", + "js\/wizard\/wizardDetectorUserGroupAssociation.js": "595578d2a2ce0f7ceaf3e702536d429b65a75419823f54c00bc10f4f92a32fe4513d0e53447798372f94bd416cacc2d2a1015eedb647e58af6fa49ec950b5fca", + "js\/wizard\/wizardDetectorUserObjectClasses.js": "33dc057794b5ff169a830ca735f09c86beda21b0d166f15f1870006c5bde25fdf2ca02ccf936a51400b2873d27da95039f227bcf63d3844d8b272fe75f726cd2", + "js\/wizard\/wizardFilterOnType.js": "44f8424bdff5d5af5b34d52ba8e719a7cef8331d49a414c326a6ece727210b3846d37867f7bc9846e7a4897529ccf95121a2106d19ca7c3f05b885252ba36a6c", + "js\/wizard\/wizardFilterOnTypeFactory.js": "e4a167d6b4c2db7630defe05b944a38d8ee4fd358cc9b5c206391e5fe4fe35237e08f3bb511138be16fc21a5fbce6d54b55c8ec9b03ccdd33fdee250f55b989f", + "js\/wizard\/wizardObject.js": "d76fce9ea1abd4618012113bb9eb92e0e35de8277895d7e45f5cb616eec2c634078fe3ce6a41dc3b4f46c64240d9e831b0ce48906fd10a7172c15a5ff9833d29", + "js\/wizard\/wizardTabAbstractFilter.js": "678ebdf4163aa0c41a7837e7640a5afb8cd6e64cc910b21e048a1bc7264d2b7614290881b8ff1286376de68ad75b7dd4a7dd6759165bf6a7db03eaae5b892b9c", + "js\/wizard\/wizardTabAdvanced.js": "4835f550ac89e4ff7bb86ce3f4329f5038f6133ff97a01ab3e59d019f4a4b44233320dbee05f9211363f0df1f9dac44be4226b3d0fb85eba5928457204d7512d", + "js\/wizard\/wizardTabElementary.js": "f2b1987dfb53b75c4433fd64136815b7f87a6e723034110bbe1f0454fc9d459ecbc22545373f09d0f357116b18244085896bccdf92fc5f80e1a267bcd94447bc", + "js\/wizard\/wizardTabExpert.js": "a4a25c894bf9cb9cc3c46812c43cb3bfe1d5fd7ba37bc634c24be40c6c413cd29468b2d36c2c61c29ed6c3579e92800e28cd53f0fe81ff2702c9ee65bc40514b", + "js\/wizard\/wizardTabGeneric.js": "c291f2db3eaca5be532306674a11dcd0e47f28c1633f9e500aca66f6ae07f5845eafdc78efd813db037e81fed94c6ee87bfc375ed6b2ef5f5dda5ddbd60913e6", + "js\/wizard\/wizardTabGroupFilter.js": "1d8577373d5f68cb49234155553f4f9af631604bd02b57c25416f500086170b7ac8a579c82e8ed4dd9bad46ae6ec2a6b258555458870c0438d814e96e5f96b37", + "js\/wizard\/wizardTabLoginFilter.js": "645ac27c059c9f4dcac94aa16da834c873e08eca2ffa18d17569ad5aec968059d253c8081fdf8b3c813f5e8ba157e979285807120e8242345b95826623b40515", + "js\/wizard\/wizardTabUserFilter.js": "1d81ec356bc1b948762123098a52f862c642ae6a0617251bfe167bda4073a57fa619dd2be40148ce938ab245fe48178e743d85c8f47025bacc75aa07ffb9e8dd", + "l10n\/ach.js": "584616c60777f992280413b3849fd779c5f8e1b4da2f4cbd3864dc35842550e01076fad35a6f95ebed4f32436db05e70f613e95e676a83add0e36232b922a0f8", + "l10n\/ach.json": "f3711573f0f71802cb423dd9c506c602996a7fbaab151df96a39f5049ef4f15617fc771d71766cca8f22e50ce5c5319f4438add093fdd0a1b73c5344d8c34348", + "l10n\/ady.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/ady.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/af_ZA.js": "edb03a848a0d3a0b70c6e53c48169455bbbdf3d853c61b21abd06eccb9ab01a7457ae17ed07d5627e7858259a52467f1f06ceca071c3cd5e86b41aea7ae1a24a", + "l10n\/af_ZA.json": "b4b000b3eff34c4f6bde514baa7c563edb710ee1e3aa68ed2cc94a30b72118b09801606e1760c437b288f9172453a2d2c38f84cfde23ca3f39b1c3fd243aad6f", + "l10n\/ak.js": "684c1f70d99dabf5d979146e9cfbfea53c084a1fefd9c6a0c554f5a791822a88782f10011ae52634ef3d563a070e870d108f30705a47dab9e9498b92660ecfd5", + "l10n\/ak.json": "fdedcbc0ac85c73ec9e5121f515df2b2a2a35a08c0bec811d2e4672b7e609a6a348f062675c245767be3be16e4b5e54699fae6b5af07ce2d09324bab24669277", + "l10n\/am_ET.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/am_ET.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/ar.js": "722a73cef6e346c23d7a04b9e29ceb9fa91f6d92a6ae0e89d5722972a753dbd62f057704b1ca762315c2ada3d7afc227a3f4f1f988e671a12dfc9801f10632ff", + "l10n\/ar.json": "90691c6cbaebbc304979c248426a46ede9ebe7aadff04138dd83e0141f77218bbbd38f01869dc08da1215efb51d334dd662f96c1fc499339793e2ceb6ffac054", + "l10n\/ast.js": "8c208b78db277f8da280323dbf58b0359773fd906330b0fdf499dc677b244e674f7947820343e079da88337ce3e8106b6c4e7a62ef23d146aa5dc720eda0151b", + "l10n\/ast.json": "2ff61e52e5580d95d145710b04c2d53fea4e2fdea09d16fd5ee9ed8dd9edbd5116cf7c040fc65e1d41d74040f32588bd43441fd6ee15872c2ec1bbded8888b22", + "l10n\/az.js": "e3e2852fce6e2e87a0556f0370872eff673b7ea1483c0b483212bde0394a0ed8df4ca836de080d6a7e9f7778baf415a76ec03d4615ee2894d99407547d9c9d1e", + "l10n\/az.json": "14190932496abbc90e7ac934f0731e5b6b3d5982f27881a60a55136404cc97bfc6bb9e3528bd4cb7752545b14bab2b0b7e5a96d5eac7c8f5e9bf3633fe787d3c", + "l10n\/be.js": "5d6f40227af673a9289b7e2adddb328440fd3760481977281f7c1f2a8d04d4c4a8c75a7a843aa66f6aa8fa1153de4b40c7b8d781692d3036d4889b68ab34e26e", + "l10n\/be.json": "59c943178552b703fa989e3cc1c54a56205c0d8e1a1feb3cc4f1a88178eb289579814612f6bd884092d8540887dd1829e0cbd8c38f9557ca459f80cf7bdf5b56", + "l10n\/bg.js": "0e5e43e17003393df3cf71ffd2de1b6cbf256b20c1be4bd5b2f64aacf417e19d74dc021fb970e836c0c3fa4f7ce02c5ee9a37f26a56be4d38787eb110da83ed3", + "l10n\/bg.json": "3a9e4b0cc02f1640c7799b77fe9c46d99f12105a9e620fe03dc53903b2d20a4db8d48bc96131c01a22035fcf75b206dc5569cdac54faed8c63ce29f0db50a15a", + "l10n\/bg_BG.js": "2ec7cc4a257f4c0e6c4ea7a2e3e0520b97b88c9bb39c5a73e96dbae324f7c63382bf7483863b79b39fcbe1bc4324aaf8735a913d2cefd02e2e2452cfd3bc6e96", + "l10n\/bg_BG.json": "d6ae3f44ba4ceb85faf869254984aaa54d0827b686f4cc2e0434f3f8a5024d8a406865ca6c911e0b5a4d019c283fc055b6f02a24559537db7da1c5ef45fcc56f", + "l10n\/bn_BD.js": "b494e45b53a4c475ff98cdcb6f82a4b4fddf4b87443743baa8ccbb8cf0b4fa7992189a881aca90485e9ddfb3bfae6af60db6bbd1808d06e72bc91806039707e9", + "l10n\/bn_BD.json": "150e514eae5ff7dfc65366a9046da727552595ce9a0096d9a2543ed3505ff0ab07014e64354cbea5321b07be0bd4dd1074ae72810e52cfb9b22c76702c9de76f", + "l10n\/bn_IN.js": "dce780e83554e4e358b1f71d50b1f61c1c7fdde664f32bed016679cc342f99cafd5775d8ce1f83b6930712e42df978032d1874a0ef0d12440b66d5c3ccad5ac5", + "l10n\/bn_IN.json": "5f6839abb9432884a712f402909729f5f658e7405c91ed438ab2dfdcc9287d32a896f8547975c11d209f64e00383be8c224cbb3d232f1ce3e60e7b7ad451df28", + "l10n\/bs.js": "35af64cd9ec04e8ccd830539bd812534879505314a1c717752bae305f7280fded2e4cb3e91a28b5209436de780eb040d48fe443bd1427d0aae8b14b97f335123", + "l10n\/bs.json": "b452d8d5e32b4be8a224061d1a10c090bb6360f7a85ab370195d999cd4bd57cb1b3caafbdf2e0b78cdc4af4c0bd191a988bad41d7d637dfa762a8b4755d3226c", + "l10n\/ca.js": "7f1970d15bde6c238e52486959b9561cbb7eb404e065b3b7d3f2a5f5fb28fae08254577e96403798cfc31399584016e6ce5676e5ba3f4aeeea0bcb8d6d182b46", + "l10n\/ca.json": "c99f641bc07b69b135b710a36b99cba9cc8723781ab1d5e1fea9385e4eb4d8d4ecea2ae0b2ce272db182d70d1f79e678b6c32213cd7ecf8766c72f167e084069", + "l10n\/cs.js": "2172e1b28288977953991e113d23075ba76811bac09a82bd01e9d1bbd91605ac9bdfe5fbce173f03d2444b769f3e5e9f04cbbfb39c35a3b03711e847cb79a279", + "l10n\/cs.json": "36a35057bc4968fcfea18be71c528eb507032f6de624cd8fb37f040dc12b08f39960f48276f2416f939cc2a4057f1d00e8fd35203977b618bd934d1f86677f1b", + "l10n\/cs_CZ.js": "5479e2fab99711ea57ad9b77058f16035134a6536d1c6100bcb24fc222f5f2e8bdf5373d0cc01c1a190cec81681b566bccc8091877fa0f3e1834b0039fa31bbc", + "l10n\/cs_CZ.json": "dfbeddce0787fcfff9330ec4c5066c571a77f9ca4a3ff9f009e2899dfd4b8da6f0e9e594ad9a16c4319944a90a66a61755baf78319e326e4a46fa7ed6f42888b", + "l10n\/cy_GB.js": "44b83608ff038ae1af4991fff868f747288204437c28162de4785f78891815f6fadbba30973b83755a7c30616dd595bfe5d0bc1e7538120751e470ff24c4f1f4", + "l10n\/cy_GB.json": "9d583b7df484bd674ad4c76a3706588e56d252fe26ee40a757a01dc2a80960a53f366755be605441678d99487b4bda167e711520fe09482fede22a2798f1d9b1", + "l10n\/da.js": "de73f0241b84e8c37b8786dd3f2a245e4652f1dd29edb6f9dc278e1a0c11d0c20336e9147d59fe6a3fe952b4e444e82a9cf1a2c9c118ab7eef78f0f1a8a288f2", + "l10n\/da.json": "33df7cee64fdf40d2a88fa637995f294b502b6cae988cd6c017491d47e578aa31599adcd5795a6d885ecb5f2494fa5f36d49591ac465606251244ce045bde54c", + "l10n\/de.js": "fbd641e9f94db3e97c8a86c6a31dec9059a88940fd58efb58c8a5e9eba1df6511494cb58aa7e2df9f7c3164e80f1c0c8a8722173db107d9f23b2f5fa082beef8", + "l10n\/de.json": "4ce84e2d386a75af10f40625fb7dd5f20d8a0db6c127d93aa0a42a10b19f508f04097b9e53f476bc5278ff2af4a3d5428d1da1bcd2fea7cbd304b4a98fe281bf", + "l10n\/de_AT.js": "935c48fb9fcac9313972d69c9beec9dee7836b108392ce77a2c060fa3975c2932c9e351da9c14faa47d06c5e03793fb2708ccf0564119f4c1dda25f392e5a361", + "l10n\/de_AT.json": "448db53ae94b6a2b82714e574c5b41ea2b58fe783db77b591a11300664220c47e99406bb515f5b9615e43bf6baff792f3ac7b301cca07f551e1fae9884bbf191", + "l10n\/de_DE.js": "152337ba4398a54035f5486f37500a07d3f161a29cc0f3da6a14f4193607860d35010ab4fd738d217ff6c70769be7c77cc412d043d5cc27471e6b1766d0efedd", + "l10n\/de_DE.json": "bb220a355c1d1a99acf426517a5db2278b257bb2d75283eb433d826dd37a8c7940f843d6a5ad6a80f2b3170831a50251a9362557b3e52c47af027d423908c4fd", + "l10n\/el.js": "73744173946781e439f76eb5484f981d5a4e26a3d11e70cda50efdfc20d86a707c3c3ad336a7e5d293aa2d172b1085555addca002924ccb12f1b324d3e4d1cd7", + "l10n\/el.json": "2fe36a2c9e0fa582d6f04d2c3964780b05b0cebfb143651973b25a9dc362cb3639dc6c7ce1b4d8bd182fbed9afd782a62ce063fa7022bd34fe88c943ac80817b", + "l10n\/en_GB.js": "c5e830fdebacb1a122234384ebee1fb89218523f46e7d58b3499a2d9593323802dbbea3bb6971f4f1f1309c1619c072f7fc91e1894153092be16a905c83dd4e2", + "l10n\/en_GB.json": "63b2eac2b3b0eb4f763347469dd234583b659c26c56b9595c9e7a918d75d8b284fd3574d6f7c8e1a7abb3b3b48ae116c94deca4ba33d9d2fd342f546d46c0409", + "l10n\/eo.js": "4769be5b1e7e1d688db0ce7688a94735e4e02cf44066a54e4c1a4dbff986777c3589bdf6a781746cbee1023d73a752b8630a27ae1bae10a394ddaca537d319c5", + "l10n\/eo.json": "036d8e3864ba0697427c16671eb600cb64c46e5262d35af89e6b6dd97205e0a69d97f5f31cad9c97231c9cb35d25ba5c863425c0875680a66d54f434681c80c5", + "l10n\/es.js": "19fb9a3925ac7088735cc3e914bad4e655c228d9bbb254968ac6c350c4dba7be9bc8e9c5a7f4242a5ce8c71ab0a7dc31e94baa30814e018eeadd887f5ed0c4a2", + "l10n\/es.json": "41faf1a1a3d9bb80f524f0180d47adbe31c754f05159964ce3abc8f31670b1babb9d6850cc6899b618726ecca4e914063d16c5b149f263122f47cac6c15c3b07", + "l10n\/es_419.js": "21b666893116d3eccac0520c9364e144bdadc6459ef7213ac60374177eebf30029fa40ff083be8e064eb20919c26cf32f715415223edb5306811a63ab329afc4", + "l10n\/es_419.json": "ebb7e09c77402b01b1c737ba2be47b232d8d724ff0fd9a0473e1c7574d8d8fb29bfc2a27ec099c1381a0f2066fc94748d91cb8e67267cb2d7321e1a586660fe4", + "l10n\/es_AR.js": "9c0d8a0942ced3ae7f0f9b8e0f3c310d1d67658850e233ba8bea395b7008446a474daf095239c5a9dd50eace25a8d659e00c51c07943479b1a5d6b40dbecfe0b", + "l10n\/es_AR.json": "3142e5ed3ea004a2e8f7304ad8c92b513026cc3cfd095159b4de4c2c5cb0a41c8ea962663adcb8d7128ca5d3f5f3ab9f40f9249e28ea36369259ea3f01f31e1c", + "l10n\/es_CL.js": "7664b8affa1ceaf0a0f49d806635185bad0b1a9446505bfdbe47d0eddc76197c19983ed6bd90aaff4fc309319b8efb81478c78b2f338e8376e5ba919d9ed80e2", + "l10n\/es_CL.json": "a4f44ea57e5195ab3f859dfaf8509769de4741d05050a719e53b369924d1a60ac0ed74ff427af27e30dcf256071f14f84579fdadcffbbdfbe99dff4ef5879ddb", + "l10n\/es_CO.js": "7664b8affa1ceaf0a0f49d806635185bad0b1a9446505bfdbe47d0eddc76197c19983ed6bd90aaff4fc309319b8efb81478c78b2f338e8376e5ba919d9ed80e2", + "l10n\/es_CO.json": "a4f44ea57e5195ab3f859dfaf8509769de4741d05050a719e53b369924d1a60ac0ed74ff427af27e30dcf256071f14f84579fdadcffbbdfbe99dff4ef5879ddb", + "l10n\/es_CR.js": "7664b8affa1ceaf0a0f49d806635185bad0b1a9446505bfdbe47d0eddc76197c19983ed6bd90aaff4fc309319b8efb81478c78b2f338e8376e5ba919d9ed80e2", + "l10n\/es_CR.json": "a4f44ea57e5195ab3f859dfaf8509769de4741d05050a719e53b369924d1a60ac0ed74ff427af27e30dcf256071f14f84579fdadcffbbdfbe99dff4ef5879ddb", + "l10n\/es_DO.js": "7664b8affa1ceaf0a0f49d806635185bad0b1a9446505bfdbe47d0eddc76197c19983ed6bd90aaff4fc309319b8efb81478c78b2f338e8376e5ba919d9ed80e2", + "l10n\/es_DO.json": "a4f44ea57e5195ab3f859dfaf8509769de4741d05050a719e53b369924d1a60ac0ed74ff427af27e30dcf256071f14f84579fdadcffbbdfbe99dff4ef5879ddb", + "l10n\/es_EC.js": "7664b8affa1ceaf0a0f49d806635185bad0b1a9446505bfdbe47d0eddc76197c19983ed6bd90aaff4fc309319b8efb81478c78b2f338e8376e5ba919d9ed80e2", + "l10n\/es_EC.json": "a4f44ea57e5195ab3f859dfaf8509769de4741d05050a719e53b369924d1a60ac0ed74ff427af27e30dcf256071f14f84579fdadcffbbdfbe99dff4ef5879ddb", + "l10n\/es_GT.js": "7664b8affa1ceaf0a0f49d806635185bad0b1a9446505bfdbe47d0eddc76197c19983ed6bd90aaff4fc309319b8efb81478c78b2f338e8376e5ba919d9ed80e2", + "l10n\/es_GT.json": "a4f44ea57e5195ab3f859dfaf8509769de4741d05050a719e53b369924d1a60ac0ed74ff427af27e30dcf256071f14f84579fdadcffbbdfbe99dff4ef5879ddb", + "l10n\/es_HN.js": "84c1cb470619f5aaee8221aefc9d3c64c90d1491183c71f88dae423d193ca49b993b8b52a8ef2f9166ba901651826ece641d12151e0e1f61feb66048e7eb0c11", + "l10n\/es_HN.json": "954f70d6c3148c7ba762596f0bc78ffe0097d36fbd34d441e4010ae48e2eb2608f37fc400f1b59f2bc9289ae982f8813624b6c2a7bd2854d69cb2821e40dc4b3", + "l10n\/es_MX.js": "1411e553bb73bdc7fff1a58f32c48f58fa0a4183e20921c1564b10ee6766bc31ac92c7cdbb1870174d5a4a5acb93f51d7dffb3bafbeeab01732e767345aa6ef4", + "l10n\/es_MX.json": "8f503a97d39a91f66f664b732b04d4779e081fdaa3a45571a9ac0191463e02a0ac6fa979fde434b2e05dfae46d08f5565586ab5a0e4a291dd70e14e4b4e95aa9", + "l10n\/es_NI.js": "84c1cb470619f5aaee8221aefc9d3c64c90d1491183c71f88dae423d193ca49b993b8b52a8ef2f9166ba901651826ece641d12151e0e1f61feb66048e7eb0c11", + "l10n\/es_NI.json": "954f70d6c3148c7ba762596f0bc78ffe0097d36fbd34d441e4010ae48e2eb2608f37fc400f1b59f2bc9289ae982f8813624b6c2a7bd2854d69cb2821e40dc4b3", + "l10n\/es_PA.js": "84c1cb470619f5aaee8221aefc9d3c64c90d1491183c71f88dae423d193ca49b993b8b52a8ef2f9166ba901651826ece641d12151e0e1f61feb66048e7eb0c11", + "l10n\/es_PA.json": "954f70d6c3148c7ba762596f0bc78ffe0097d36fbd34d441e4010ae48e2eb2608f37fc400f1b59f2bc9289ae982f8813624b6c2a7bd2854d69cb2821e40dc4b3", + "l10n\/es_PE.js": "84c1cb470619f5aaee8221aefc9d3c64c90d1491183c71f88dae423d193ca49b993b8b52a8ef2f9166ba901651826ece641d12151e0e1f61feb66048e7eb0c11", + "l10n\/es_PE.json": "954f70d6c3148c7ba762596f0bc78ffe0097d36fbd34d441e4010ae48e2eb2608f37fc400f1b59f2bc9289ae982f8813624b6c2a7bd2854d69cb2821e40dc4b3", + "l10n\/es_PR.js": "84c1cb470619f5aaee8221aefc9d3c64c90d1491183c71f88dae423d193ca49b993b8b52a8ef2f9166ba901651826ece641d12151e0e1f61feb66048e7eb0c11", + "l10n\/es_PR.json": "954f70d6c3148c7ba762596f0bc78ffe0097d36fbd34d441e4010ae48e2eb2608f37fc400f1b59f2bc9289ae982f8813624b6c2a7bd2854d69cb2821e40dc4b3", + "l10n\/es_PY.js": "84c1cb470619f5aaee8221aefc9d3c64c90d1491183c71f88dae423d193ca49b993b8b52a8ef2f9166ba901651826ece641d12151e0e1f61feb66048e7eb0c11", + "l10n\/es_PY.json": "954f70d6c3148c7ba762596f0bc78ffe0097d36fbd34d441e4010ae48e2eb2608f37fc400f1b59f2bc9289ae982f8813624b6c2a7bd2854d69cb2821e40dc4b3", + "l10n\/es_SV.js": "7664b8affa1ceaf0a0f49d806635185bad0b1a9446505bfdbe47d0eddc76197c19983ed6bd90aaff4fc309319b8efb81478c78b2f338e8376e5ba919d9ed80e2", + "l10n\/es_SV.json": "a4f44ea57e5195ab3f859dfaf8509769de4741d05050a719e53b369924d1a60ac0ed74ff427af27e30dcf256071f14f84579fdadcffbbdfbe99dff4ef5879ddb", + "l10n\/es_UY.js": "84c1cb470619f5aaee8221aefc9d3c64c90d1491183c71f88dae423d193ca49b993b8b52a8ef2f9166ba901651826ece641d12151e0e1f61feb66048e7eb0c11", + "l10n\/es_UY.json": "954f70d6c3148c7ba762596f0bc78ffe0097d36fbd34d441e4010ae48e2eb2608f37fc400f1b59f2bc9289ae982f8813624b6c2a7bd2854d69cb2821e40dc4b3", + "l10n\/et_EE.js": "d19d3a90974cf7da770a94c6ae731260d0c1ed69bd553402d948dda9673cc55a635584fc0e7f7123e8e38dcf4c51cbbfbb92de3ad8db33a51f4cb795799eb842", + "l10n\/et_EE.json": "cb3ef5a466897cd634f1a7958fb148565866ad38522e3c263549c1b212d597c0bd233cadba093e2c801c16bdf588500f050e45dae6b26269eaff9aad8732c749", + "l10n\/eu.js": "c47d5cde2c8fb1bda6fb8449c611d24ee3d2f4b84a0f5b0040fafd9f2729b23b7b0e70dfe3a7c077054a071d4e8c03d74a96fbedfbe2d53ec37e37652f284f48", + "l10n\/eu.json": "56daffd076c4992b3fd2d6dfd990a286f1243f0018fc11f4f6e76fe5b0017b4c65aa24c5640390a9cbf44adc7ffe6cfee1f34b0202139e373bd964cd723b7735", + "l10n\/fa.js": "64c180334ca9205284a7fa587f10b4bad40519d8564b7b357fc829407b08a92afbbe0fc5468cbdec8e745fdad8a242ccca2aa5b782c5a29d48f8a10a4ac0fe73", + "l10n\/fa.json": "6b96b3e14967d5499a7ebe5cac45476a54454443a058bb87784ff03ffe49905a0bd109cf876ad9ab17ad299e7ca78e35e42d41117f60b3c1c72c2ffbef253762", + "l10n\/fi_FI.js": "5c12a666f25465b47ccb09f25ca2d57f8f1d2a671112eee61387c20b1de641e73eb2792c828e1f72c055ffb106133128e4cfa5eb9768d6bf6526e2927ff50f31", + "l10n\/fi_FI.json": "32780f122b51ba44e70c79729aca66518149107b15940ec3347859e1ab0a31db5a8bd18724cc59e074a2eaf0ef1aabae98679362fcf2258ebc48fe5ed44763fc", + "l10n\/fil.js": "c868f0f535d2a1d528705dc9795801d90a610d8c7a1f765aa370d92bd6c2e4e32172096da3adcf862bc531fce8dacd31c41b5dcca375df08d0657b4444ace76c", + "l10n\/fil.json": "7844263c6b9d31cfe8f123daf7a710f87ab3ae58f30d252625e3b78150f3099f16c7a37c9b0199fa695953efa2767eaa7f837f34df60df16c7da4138c2243af5", + "l10n\/fr.js": "4228bcdb639b31f6d103d4dabec98e6ee74c2e83098f0b30f82d1efac6e807d8bc079adeeab566f2ebe693a6ebdd93e2793dfe34d356dd8bb85fc46bd0eb4878", + "l10n\/fr.json": "cc62a016ff687a0b4d12ce5859ac4d3d853fa001be62b8176a0151081792a32e0f730dc9216ea55b38b5086e7aba62314e11506cabf6c215fb5a0a8bdaec43fe", + "l10n\/fy_NL.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/fy_NL.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/gl.js": "67d7f3bc6d807ec4189c8db50f96bf2779dc3952c531f3ec5f9f0b43e996e2f817850974096d65cd2c4cbcbe59264aef41546cdb398183deddde03f1061af2f5", + "l10n\/gl.json": "bf0972e27d91ba85ac0dd95be98febdd615497ed36a22549d07139a1b63382a2c3fd8a0e08e5fb556b9d251a11450a8aa0397f6f4fa4b74bba5aabf97acbdd08", + "l10n\/gu.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/gu.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/he.js": "494c4822c956d1121765a70ce53a7130b4b2c165856a1f2ddd661931f81e2699c88c6e5c06eaef9f19880215e5d142c3d5a58142dd31debc7bc183437b2f5a68", + "l10n\/he.json": "95565877d42fe0497deabd94fd1a64bb5b48ba9428f24074b89522e595bfa3932458a6f6a38e883339e255a6622d56c000bcffc7832a0d0642cdf8158224ea05", + "l10n\/hi.js": "a986c52af4ba78092ef8b5726b1a270c1195819c830e84853cfe5d8680c28c84987c34fca1d10901b4e7c74f99b1f12c9f5f45fee0dcb5169d3cb894812eb423", + "l10n\/hi.json": "bc9d722b0d556f763890ffe8f38b0b1e1287c52ca886425bbcdff216503d9b7d539a58f1be906d5185461bdb4e565103d7aa5e675dfe438853ac8e9e15d002aa", + "l10n\/hr.js": "746c8e816eefba860746a31a1494c50b725aef9303a557266aa1a934dd323aa92612f89088c41dbec93a9d13db3c48b6ab70bac4d3b46177de6499e31966f19b", + "l10n\/hr.json": "d73a821c7efd35a363b374a525c1ed087ced68e8ec443cba8c4d43f72f1535f7873364fcab05595d5de935b1d2a8858f96656cc52a96b48491ff43b1b9586f31", + "l10n\/hu.js": "f4b2bbeacd631a6fcda3b149d4725f6cb585076e383edb6a011399c29b98800096ded6126096c77235053755f11ffd619b6ddd9fab62516f6c0d74d9b2a7c635", + "l10n\/hu.json": "e2af504727210cf5913a74e56647e4dd481ef39a638723edc589d2570007ce3aa0e2c2ef4d9a36a381f772fed5a312f693f6bc562c797d46e68e69719f76c21f", + "l10n\/hu_HU.js": "b9ec1d91df19c6849d210db68bebbec62420013fb3a236eb77403b166d734e880ffcdc55cfaa2b66996c7ae80941dc6cee55dc9b5c9ae673c564ef4b0ff70505", + "l10n\/hu_HU.json": "797c57e3aa71262fbf4b8ae38f87805307edf0e8f1e7755647fbbc2a3db93ae5046c9a85ffdf8df3df88291206d509350fb19a4b45dcaadf711cac25c8a24244", + "l10n\/hy.js": "28be9adaa7b7dd2164c563f9aab7b744a3ad6b57f05837ed8f847e8e9d7f9181fd0d06461a9cc6a26877bb605e5241041475ecd733082d775dfe45aa385ed29b", + "l10n\/hy.json": "804c07cc49b22c1deb07890fd2036b371edbe4a0751849f185c3fde9cf01c6ec8998e7ee3d9ea7ce0892621d0161bbcd5aae85b5e287ac26145858c58e0c468a", + "l10n\/ia.js": "488245a1d513c7f2bc563d5c67b0db4f5ab7ba88cb67964fb513648c2e100a2581f95ae4c9eb2bd143d470d65c962eb15e0a3775dfe212edf258aaa49efacd37", + "l10n\/ia.json": "618133a3b6d8df8e9993eae72ff07405b1639f25754708958fb43ddd9a36a4c0868fdc539b3e2c41b2b9fac5cf187279a9e6d21fe7461ed1f39f7b21a98d89c0", + "l10n\/id.js": "51e612a4ed2a03ec3cb7153fe0334f18b898061313741e8882bc8e89ecd0c502d781eb02fd96b8840c39bb9ec04186122576aae8aa7e0357299c62306806f012", + "l10n\/id.json": "9f1382816d3c71eed55aba0461c4050fabfdad40b3d6303ba397bf6a18144845ffeaa8bd580fedbb9fa4f77ec67b2ba15fa29a9b2ea34ac5ad58092889b4dba1", + "l10n\/io.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/io.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/is.js": "e729f05c90ce0874a92e3dfe5cefd13ee950b70fb1ba1b13af04b3f3f9900e850eb7d6c7ca7705ba4d30c3e6495ba11d2a19cf8040ec86ff471c940790cbe254", + "l10n\/is.json": "4dc220104711dfe63a9309ce576133bf186e1f64cb9e2cf46c6f18431c2f60a6ccc78a0eeccdfbd5dde7fbde78790ba14867fbfe38d46881e496180d596022bf", + "l10n\/it.js": "d520831396d8a3294379d286d67e1dcd7b02f97dfc4389c96291810ae7bef278bcc4fb53471768e4d17953f856b0bb0eb478b75d81a7d3f44dcd81ffc65ff518", + "l10n\/it.json": "66be379ab6eef655fce00f77f30bd378da4067960fff7b0f959289035be38fe4d1baf97680eeae0fbe73fa780cf4a5d4a76c975a7cffeff8b78e7f901dca91f0", + "l10n\/ja.js": "0f7a9a7c60392be208a4d20580005027e5362e0498137080540b31db8a871dd660584382287586d3906c696ee852b56c7d1e9d847b32c9aa1eba1f0bb04d9cae", + "l10n\/ja.json": "3840073b1962995677730ff1a6a411f7e636161803a40474d0bf7af9e8a4148feaf0b2a5c4adafca2ce226f6d343312b9979bf220804a56903e85517d3f9b521", + "l10n\/jv.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/jv.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/ka_GE.js": "b06bf83267a8e77fb19a9cc4d5d6b8af2a664c791bffbf9a7df7c02088573e5f3f6a67b9050aa33d765833dbb7ef62ef6bb0b83d0db1886e48ca897a196de708", + "l10n\/ka_GE.json": "c3efcc5455e3769bd704db9aa3ac7452f31790ee13d66faa81392366fe633044c3543d79fd8bc57bfe15112167211b0eafeb5d30584311b6fb6a7408b4b17c68", + "l10n\/km.js": "557c1bf0bd7219d7de10270639e674884d1ad088ddbf1d69a149e983b31588285dfac7ef1bbcc345d0093a01b1fedd2bae61bd99a2a407060f6d2b718adca2c2", + "l10n\/km.json": "0c4181d193c2900bc6cd2c11ac8b3550fafea4b9d9771bcead54cc31fb7e9686e890ad6c1cb9013f3b46a273669b5fb79b66472a89735efbb6599ff48c347e09", + "l10n\/kn.js": "8ea32b354ad6fafa8163c4e0fd71535d00da8f5e1a9e043c7a1aac4694581a4013b75e6e6aec3c68f365073f82096ba2e1690c342647dfaca56de30d1e515074", + "l10n\/kn.json": "095e1dc0efd11fc03456fcbdcddb933ab2d35a34f6e80b6398106b68ca46545c23fda045c7c70a1ef5882b48f17fae5936253ae81fdb39af89e7c2035a985700", + "l10n\/ko.js": "060326aeea20bdbe45809ff632f47a984f07e634c92186841c182e924bef170a4a2f0ca97f9e14452b9736d9faf73323ccb1400a4506fdb0798bceb8443c0114", + "l10n\/ko.json": "02daf105d3a8c34aeaae8607e6650d97a7a509652e6c8d7ade48d5e920a1d13e04920388d1976b399dfadbcf4e54eaa94c2d226d29826914e130e975a50c49bb", + "l10n\/ku_IQ.js": "0dfec555d8a4b44beaf255742023ce82505773094d0557ab15c713713563e83471af7820a94b32609a097d20b5be01852a4a7f957a3526ec2fed392246765ad2", + "l10n\/ku_IQ.json": "d77052bc951b1c1402ee001854f5fcf9d649f35f991a134828a8967e46298aaa673fec136f53eb0eb979852fab170ee851cca18d7bd9c641567a6cd1512082eb", + "l10n\/lb.js": "75cf77133674b06ade17f149ec45e56d8a8545df01d4a796a0b34a8f2d0302c97437d4e5bcb4b70626aa520cad2a11cc25c37848da80bef9d64489c236952c08", + "l10n\/lb.json": "1a907088abe0a783f2eb4dfafbf6ea0ef51ae606fe2fde9cbfa52b409db859fbddb35d45afeaf16260367dd59978fee5ba76c76bd463b5a3f535131fbb161dd3", + "l10n\/lo.js": "71e16eb185207be77e8efa486734ae0f4c5e639cdbdd799d9a0f1c59e966d0c0877c10f872a99a4d0489a2b32ffc76efdd09629f6b587f57e5d18582392b956f", + "l10n\/lo.json": "8feb98b74cc4e22fc60b147fbfe34148b407e37ea50e74cc8b205226a3db47d592ffb1125eced07fbebe9078f961f85df3ff6c33744082a7cc9a59db9ad9269b", + "l10n\/lt_LT.js": "c87abfaed4b16e87d8479a8857b53a0cfbedb7ce0359745189b6768e45214bbfbd9898c10121fae7d0dbe74330ec53b7b66268e00b250e947a14608969005725", + "l10n\/lt_LT.json": "bc65eb3930ba3115de9248f124bf83f5cab0d0daa2aa009cbc2c4e639f7107ff51cfc6f97ee52943ce4e10402a5654efd0ec3ef666d636e7474c3ed4d9f93958", + "l10n\/lv.js": "578d55d2ffca3df7bcf6bf822dc53d01a413cd3254e7becc3f6c99983fdf97376912b46ab1302f7ff4c4806b9e43b5a68fe51ee9d188eb1aeac3cd5cc12b9f1a", + "l10n\/lv.json": "3d79ba3206f961b53ccf35653af394c292d2d22df3fe7d670f07aba765c844291d932196514a49c0d607d3ac19a57ad7e52be7df86794932e5e71849ad07438a", + "l10n\/mg.js": "584616c60777f992280413b3849fd779c5f8e1b4da2f4cbd3864dc35842550e01076fad35a6f95ebed4f32436db05e70f613e95e676a83add0e36232b922a0f8", + "l10n\/mg.json": "f3711573f0f71802cb423dd9c506c602996a7fbaab151df96a39f5049ef4f15617fc771d71766cca8f22e50ce5c5319f4438add093fdd0a1b73c5344d8c34348", + "l10n\/mk.js": "e1de68cd6ab6ee18ca877c29472b3d51670e5d531a92f8c6c1f6b586a4dd7b275a609edf766a8bdf1ea1ba26f15d26b29e3e6c7fd701a6c04b43fbdf4ff0de07", + "l10n\/mk.json": "c6142a36b4d70caf5775a80e9bb11b9f2f8afe6d71879f96fea1712e80e6cfc9c2df993b2a1c6a0f00971e8b022e4157335ad5d298d0d0b09a4dc9e5d114a069", + "l10n\/ml.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/ml.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/ml_IN.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/ml_IN.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/mn.js": "0e2c26e565a2ae53ede2163b5bbc8972a5f1064f98d17acab0fa68238289c342026166679a2a1019b643006f2c8daae22a4f27ffed82dfd74bcd65a54fcf4e55", + "l10n\/mn.json": "180297b8d0dfb8cabbaf576adfe452df26fee9fca88f33f63a1af964cee633ab6170ef33346f91991b42930603a44b3b6a75bf611c0d0a44c6948ad77409a60c", + "l10n\/mr.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/mr.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/ms_MY.js": "7733fc4a26e810877d77382d7ebeae209c8f1aa5f72f5528345f9daee2b7cbfa7f2e930a5a5f5a1ee8bf024fe3ce2556313cfd53a2788ae5553a4ae9ae51288f", + "l10n\/ms_MY.json": "3496e34b48fb8957469211e293f49cccf6301fd77206b8febf0873e009d1bd32ff512fc10d9ccaf8e16691965226788be7e1f1726e33f84cde735431332bf5e7", + "l10n\/mt_MT.js": "9e61dfcffa861ae96f054fe045d24544ad45ead40a56e25510d1442c254b89558b27f687f1c1dcd47e6e5e246446c8cc18b2e95401a4641f5e1a566c438ab7db", + "l10n\/mt_MT.json": "416cc992378a978622e5be64c58e6574a0afd3b667f43b878511070f0d30e99c98796438a51b2262ff3d711d6c058f16f917e43e5629d9f825807abb04896109", + "l10n\/my_MM.js": "d8a64fcc6e0ebd6d6bdcb2bf14536ae752aeca9959af92c91a0f15da677ac6fa14e5950d95959e14181c13a21bc8cf531543c2162430aa0f1620f2fece60bed3", + "l10n\/my_MM.json": "10d5f0de5741ae2bf6b9d57459a604b4d495f48ec29546cc0989435d6b2c3a63cb921678146889ad8499008a7e6afcf397b2260aa7f4c2719b74a2fed8ed5c16", + "l10n\/nb.js": "b494829a82fdbffa2aed72e48815ad12f558201251dab036ae320d75b963754b0d68d856649d9ac91d38003571aadb3865396582b0593e4c9b38d9f1ecbefbdf", + "l10n\/nb.json": "a1f2c8423b57f69a9ade086cd264d7427c482e7a8f9f01d9e89244b2af96950e393c67325d833157c718c9e9d039b0903083abec580ed77cf6c45e255250ab88", + "l10n\/nb_NO.js": "c189ffc496d2e9caa2553dcc01d265270759683281d2db99a7aa90b1ddf37d16c7b08d9eea5aaaf5374a1c0ec8ed75eda849d3167f6b6b88bf2dcfd6e68515f0", + "l10n\/nb_NO.json": "55bd15f5b8c45de595c5f63ca9d84c914136b98e7ca687e5aa4239312ee83dca90a0824c534a0a6ba6e41d86b8d9ddcf4a7cc5adba0d8e675d2c9a0922c63abd", + "l10n\/nds.js": "347919a68917071a98fd893c6ed9828882c96630d435fca139d39d0915dde217b477f810f2ed0430fe9d128e4ad5269c24e7843836069d585f8d8846e95ec06d", + "l10n\/nds.json": "2745b67d31fed43d75b93518d5e0c19e953f29ff347f8c368579ab81e2f297c3e60ea3ead89c5bca323841b18a92ed50e5388539c672ea3e6aae9ca01bb70009", + "l10n\/nl.js": "d6115c34851e7439172de55f70f7c5de3d501a48caccfe4d134ec3cfa44b06cfeb710f20faaa3bcb5576cc3f201fd5532f3b4d56ceab0c106ab97dfb1e70a73b", + "l10n\/nl.json": "ee24d1345f02f3a1c752d0e6150e9ac977cc32d90781f45793adf647c86fb4b5b397f794e645c2ae5b54b56a11322db8626b69e8f74c6ff5d6030feb2fb5d5d6", + "l10n\/nn_NO.js": "e6b991393137b4009cee5b5c6d4f98d992a1d1b3ba0293fe2ebaedc1f5c0bcf0f81bee58414926968931f1fd1180afb4166c32c8a389ddc217801f3223270db3", + "l10n\/nn_NO.json": "d4d39a7c9fde982c00d82b19380a88d27ddc556e812a2f1cc2b79272d0015d0a00e0fddce7acc875ef395ab1ca7a6b30d041d517e5ef96d05bf4de687b232697", + "l10n\/nqo.js": "71e16eb185207be77e8efa486734ae0f4c5e639cdbdd799d9a0f1c59e966d0c0877c10f872a99a4d0489a2b32ffc76efdd09629f6b587f57e5d18582392b956f", + "l10n\/nqo.json": "8feb98b74cc4e22fc60b147fbfe34148b407e37ea50e74cc8b205226a3db47d592ffb1125eced07fbebe9078f961f85df3ff6c33744082a7cc9a59db9ad9269b", + "l10n\/oc.js": "b5fb2a9e49b2eba4db7004143f53eb07dabb551aa7ba540bcf842825903a968945cba7140a643b76b6156c4e9a2055c995675e3103a0d7995b7a79f0ac6884ec", + "l10n\/oc.json": "c1ca15652799fa8c90aedd61811283edc658967141a342551d7851e184ba1d879bd400b9b7c2ee5ad9e24d1ddc5982bb20e4eec8d9365b1814f86cb9aa3911a3", + "l10n\/pa.js": "e400b8463e8d8976d564954dfc806ce69c863e7ac4d57967d190980db7db00f9ab2fc6c3e700d6968af41153c7b913f1517b1d29e1e42c323b6db00f8959091a", + "l10n\/pa.json": "42d8c05216a31f2fdaa6a0bed84275b3cc72577867f572768b500d7c53089f790a7c95ebcdaf7ec069b839edc82c62c09cab8f034cd989ef32a3f112cebe0af7", + "l10n\/pl.js": "f68fe730e56851a76cf2784fba74b102e17c041e1a940b5780388950045f67c46bd6ed23cf307385b1eedec413a824f0ede52b07cfdd33a62fd9828f932b2276", + "l10n\/pl.json": "2436db571356192298bff919876091e6f75cc53bcee1aca4a0827464e84a41941e696a5ebe6daa960d0b4fce29f521151545d5e2a23207fe18c2b4fc2629f407", + "l10n\/pt_BR.js": "c639fae77b0becb6f5b75e28fc428b0690d608b7732d3da8ed1ff6f6d25872630097d0c692f39f4a2396d884abcedf0931f0788437f7978bb0074fd6a7e99e4d", + "l10n\/pt_BR.json": "aee319064a407b5a5c91e33b3ea9d1d5a80fa733593b653194bf75ab38946bba7ae739e640a8fb0d1a719561f8733c1c98d5e8d67ad80977d73850dc76af72a4", + "l10n\/pt_PT.js": "4f978fb2d2388ac0aae46604cb0ecd7ff9b6ccfcd2ba46c9ceaf460bbe288b086102440519ed0bf26f4627e06b40d1c834adf83d1800905c2af0fac76d009dcb", + "l10n\/pt_PT.json": "e3d687259fe5ef5b271500bbbb0b703a45273b08b82217e043322cae0b2117ce930f1494d964ca9f374ac3507461faa1816297cace5a68e192e144ef5ae4bdfa", + "l10n\/ro.js": "3c4ec1135b96911d7d9afe58813b9fa7d2ed3719418089f2765b09f53235e519abc4efcedb691d4dabc9f4a3eb7960816ba4041e10fa679ea13cd7ca0068c586", + "l10n\/ro.json": "e0c6da762d72a55795773ae6047b1f3eea1e571a763fa44a08840d2a94a8f5c03dcadc5c350aeb6da193b3a6f3b4492554eddceb3f42de5cba671a947810908e", + "l10n\/ru.js": "cdd14593f983e84518c632c56a1291da1abd0e29c3e7186b3466ff9dde6dcf688d6473683ea7425488496a2f59c88a599f19a7e389e6c2134de7b3e8377fc130", + "l10n\/ru.json": "bf852975fb4f892eebc8c1dd29f61adde906acb770ca53df748a26fe6ca80aa6cc8a9b6ffc296f38dbf60bf79f0156a015174015da1c1b978f7c5404490e2035", + "l10n\/sc.js": "5e0790cd357fcb3b6f6470e37cf5ceb5fd475b28b8a4e69b1510d3ee4e869257ec2948df5ac9d632eda947b57fd18b2730152478beab0c320d9a8b9b2321c08e", + "l10n\/sc.json": "99ad09a4d95f6149e104aa45e53e66c4ff780c0d8a09efd3c2c1139f145a86802fa1dca1d7bc47b702be3dc8fb2da91a267e050326ffdf93a32452c412d3b9a5", + "l10n\/si_LK.js": "11d7e8a10091ff41295012b8e6fb92c61e68c798f39c5373f8bc0706088d30abd2d4d1f164a4704a9be0f8e8896bb051cbd2dbf120bb0a97b1aee26704eacb5c", + "l10n\/si_LK.json": "4289d93cab4bdbfb29b296a41f568d4d60b57ccebee1d3fcc443a2de7cf7a65dd42a1f08beb4f392598337d7298402e6b40a79d6e2cd0cdfb78bd6499e4b5e76", + "l10n\/sk.js": "5ca8ca4e5ee189a7a76045b278534d6464978a117f45012081862e59d0c210ba091b9d4d3978c5c11cc721b009eb79711c6002e14f0f7ab3dac76a7a6293aebd", + "l10n\/sk.json": "1f7066ca215833b8469af446bb95e4a4e3b55c7dff26b72b404941869a0fe45b871a4256ea99b2110f0f3de2fd3eabde509e86c64eb25b5b31f698ad098e5592", + "l10n\/sk_SK.js": "bb5ccd8a5ab98632189293f77a0118a404b04c40d80d6e706ec4cae42400fe4bdc64b7190e43838f7e652265683d630c214859c2df13e7f0fac5c72ab12aa11f", + "l10n\/sk_SK.json": "baf68108f6b8f25903da70d8d0402a48b0d352da14a7e776d1c6a08970cc375d1b5ab0f5a0b25cd79d78b247bc99f4343db3a2075939be52f2a44b3f74ce3e9b", + "l10n\/sl.js": "ca5134cb47836e077573cd2ae084d032d17c959caa9bb386ce9f892494351c482a4cfe0dcf014b0c7ddba2f88824e5b94067373ce5a2ac316260b65b0e283a25", + "l10n\/sl.json": "07134d7778a39a582fad6a6c8a86f746d367e674f90b56fcd8b526db81a941a4ac6fb31c0cfcae88dca4cb38d97c1d81710a4094500a61f9c7452be7952208ff", + "l10n\/sq.js": "d5d7c9573b1273df378c569ffb2af8bd3c4dd504423e6cb3c822e08db1fe213cc5c7639b45947b586c339ee4b861e17f9ba7ec2b23b156e45874d3966109e17b", + "l10n\/sq.json": "99eeff59b4f779d4716550d6ce4d3ec985159f580d0a0ec74aa27e4963f2d068e011583b3e73aea122afe47886f54392115096d984d13a462914ec38c8e9af17", + "l10n\/sr.js": "8e4aa6886c20f2752c24ababdbce1a29f6e63c76cc3269e23ed3d6306ed7f3893cdde94b5bcee14d483da6ebfa00640f039a074abcad9a2335505c37e69b7f60", + "l10n\/sr.json": "c8a91a20aaaba926efbefd9a4af0287293e42ddc2dc6b44b54185033bd5ff23edf4bf0b9358f197cc7009648527f8192003fd0007cde035179a8aa21a050bd36", + "l10n\/sr@latin.js": "27c449e60967619e4b4000565c772f1dc268f1dc29349df639894b2f8567b1a17451e1c25785d2a727744418775bff0cda80f8639e418c4000fa5530adbe7601", + "l10n\/sr@latin.json": "052896ae8c2ae93b2a5212386172bcf3d20d0e20febb00d26abd519390858788c83599eabe5730075b8056588cffb2868fd4ab15b86e523faf048d2e1b484a38", + "l10n\/su.js": "71e16eb185207be77e8efa486734ae0f4c5e639cdbdd799d9a0f1c59e966d0c0877c10f872a99a4d0489a2b32ffc76efdd09629f6b587f57e5d18582392b956f", + "l10n\/su.json": "8feb98b74cc4e22fc60b147fbfe34148b407e37ea50e74cc8b205226a3db47d592ffb1125eced07fbebe9078f961f85df3ff6c33744082a7cc9a59db9ad9269b", + "l10n\/sv.js": "726af9f00e64e02ae733935812a7d9e16a6d5fbd8190984e91948b64e813c233b65708b3d2e7e3ac2d55e29fead1b0306a6fc60a7ec41fa905c7e217c47f6f73", + "l10n\/sv.json": "2066133c4e7a42fc17e3b41abee63a246003a7131446efc563563fdea90881bdf308d33ffcc21d8ad87a74b0761299c333c79e771f6e7f752f2684839f5300dc", + "l10n\/sw_KE.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/sw_KE.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/ta_IN.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/ta_IN.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/ta_LK.js": "13300e4a5d50120a48e74f0485e7c9daace90aad70dbd10fdfe35431a2b776feec6ab4821900828ea655c8ece207d3c0769d1f346499af725b84d0f102779cf5", + "l10n\/ta_LK.json": "d32cbf9ae21cb9969c5a91f0e10effe033497e92302d2f99a6e7a9e816f6f53eda427730f82e953cc77b62f482b30b882b2ebf72b15e51a624a3674ec4a08d99", + "l10n\/te.js": "46f9a4c2891e920be4269cd6edb465e638c72f836e234d6c0d13dce223a8bdc9166cb28b1e752ff390ed000e489f2b0a3d96288b54f24ab6c23ea0edce037633", + "l10n\/te.json": "45840ca6a54a2fa15af397413b7f814cf7d0b5f9c24baf86988c162b67b73054a552cfae7ae97d8ae0c771d89ba6b157f2c3096d86f367a9529ca0e80539f053", + "l10n\/tg_TJ.js": "2e1675519b2c15d726505516d09fdf613d56907e3bcd3fc31c06dabb13639ab436a0b95879dfa115e53996070264b9b4f2555ff31ba58f89d4082d76a5e5c2b2", + "l10n\/tg_TJ.json": "0229170d1834bb20c391c0a9fd4dfdb43d495f2b9489f5c46ac5a673acc15f474732c222e4e5c79aa6221ab1fcf0ce298ced0b1111cbdc7fc36198dad72ac8c6", + "l10n\/th.js": "968cb2a4687d3cd22d624ee198a3a2b3c7ce131b31cfcd7364f43ae66aae1b48906db6b5cd3e6d208c25f98f0bff1e2726cde89f33224c53bdd08c37f054f3e2", + "l10n\/th.json": "a8600178381ef1ccae069d9dd54017ddba71f1446cccee20ebf8ff4402e8aa1a3df316008e1982aa1c353dcb428492ab1afd0b9f9fbcfba40f6d1bc43e65df0b", + "l10n\/th_TH.js": "ea5a57217458f1613359ac87872d307cef6cb64bf63506d521ae8c9805c0379a5ce7e39da0c9bea35d103489054e30940fd7a91644b54e6e40c8e50e667f549c", + "l10n\/th_TH.json": "ee940b017fffe5e1256db9fccc2b346bd75e1f5f68373285923907f772ebbf835c7b2f96db4077025a2b378c946aeeb564e2706308650be10ae07e527ea24659", + "l10n\/tl_PH.js": "584616c60777f992280413b3849fd779c5f8e1b4da2f4cbd3864dc35842550e01076fad35a6f95ebed4f32436db05e70f613e95e676a83add0e36232b922a0f8", + "l10n\/tl_PH.json": "f3711573f0f71802cb423dd9c506c602996a7fbaab151df96a39f5049ef4f15617fc771d71766cca8f22e50ce5c5319f4438add093fdd0a1b73c5344d8c34348", + "l10n\/tr.js": "6128fc63768ae51c6b079313fd138a056379ac0dbb39a083c30f2c83bd4a87e2d4967ba2b9f6fb1845bcaf59e5bc18d58eaaeb3cf565b08a6de397af11bee5bf", + "l10n\/tr.json": "c0ecf59df4f1fd18f0866875be099d832b5bc3566d5551b444e1088dca8874c1dcaf28b9ce621a82e7688770958f0fea996591831b8281dee3c7df36a75018c2", + "l10n\/tzm.js": "64a3c0d91672049283317be11a85e316b547ca3aaab9849b21f7a09b22d1b0a24b58e522f9b5269e08c1a9e54305c381a4f0ce6a03f0c1aa1706a91c5cb4e11c", + "l10n\/tzm.json": "13ac19c58150750ea44bb764fa75811ebe09f8f98eed65e181928b1944c6c19d3763b05bfd86ab9a31198c0ec58b2d3ef3603bbb3a77ad6be8cf2af356dcd65f", + "l10n\/ug.js": "d3129bb0be6982e1607a37482e595a3fc759b4847bbd804b787124b3822bde529fdb2ccbbda3d128f86242f1af820558c524185e6789ce69f8c24d94078ef961", + "l10n\/ug.json": "971bc6057a1c2f68d462c53741254a96afba92cc44bd53bc02500867f94a39d6d08484ef7b3d87be136b4e2534d0eb6139427edc0aedfe05dcff44cb75e0b36b", + "l10n\/uk.js": "67fbc8aa9dbb8f92e2b8e3fd5b6c052d91a62e676799346d1243092bf65c71411f9954def0ef16dcca05a945b6b5a77788da896d20c883e64e14d86bdca47e18", + "l10n\/uk.json": "2a1870f6a55b18f71733eac9745f45f6b05746cd24b6fcc99968ae2ffeec9a3c5b7d781550231447246b9ff98260248e19ef561511a0e4f9ae56e53b21752806", + "l10n\/ur_PK.js": "4af2df9528ea686197aacafd2dfd8a4d2ad579491418e564ce57a07132c620247e5d3b7026872acd5636a964b24d26e6310f80fcbf09aeb8705e3f7b00f21928", + "l10n\/ur_PK.json": "e7bc6b8ec19eb4eb552ba21f0895846e3856e0c390e6326b1b7dd82ae6ce6f19cced8af35ecb4047ae9d65a57808a72e3d6e5680e2e7a0dac5cd20f6a668423a", + "l10n\/vi.js": "d41c49aa9f19b432c0999ebadffa6add819b0e26de358de4d0651c5fa783155004d9e37c618b7716d5dc40ccf72afa02a856bbfc5ac277aab25d94fd74efcf14", + "l10n\/vi.json": "ce4b9bf4224b880a771c85449a9e72533b7013d2c7af750af5acc28da77e6bbfabec1216f96206f168d109b0a485dd70a2b97d663de44dbb49a29726c3793849", + "l10n\/zh_CN.js": "4e965563a011733ff83977e824cbfdbf3a60f9f487354c994e9a515eb2f4e7e3f8871b5e4a8201f0e81522c4c98b019109fea33584a324610ff8b5e2e1b31b17", + "l10n\/zh_CN.json": "a6c84759910240d5e7ffd2ded4cc880dc5b7d82f12406587391a17e33f290cf6c4c963cae50d6554d6181d0cac87aa741143a37530be8ced1950dcf95a3bcc89", + "l10n\/zh_HK.js": "ff17f778b71dfa55f92bd369da7eadd338fb6cb3a8488ef469faf2c55df19e8b5fbe8dcf4b58cbe379c7baf77376a1207073ec8b7f5c3313aaf45c497b90f03d", + "l10n\/zh_HK.json": "8d1fddfabde1f51b71f6d1714277a24d972a5085053183113eb9c1510f4d885c47ea9fe452a62794ffea2ba85c359b7e176cd5e57b4c905d288121282e66895f", + "l10n\/zh_TW.js": "5f14c275d04a638f15a7001f2549dde94a298fc4742305acaf06c1af61e5df1219dee571dfc748428d318ee3891b7aa1c285cec234af7181cc739f5d6d1d0b6d", + "l10n\/zh_TW.json": "46e7fa57a25dea0ae548f176f7959f00a1d63aa2eb1284bd70cb1874215b547e83fc78c34240b9d4aad0df9804ac2cf43c410a647e3af62746fd6bb32847637e", + "lib\/Access.php": "f7b11f0818fa5d5253fb36cd74346e18ced9a0c37b3f76ca1a1041d319effef9ec231d3b70fd4e58f89c32f5ad101f818c9ed352af05de0cb33c8f83f595bce6", + "lib\/AccessFactory.php": "fed958be87abcc5e65f4ce931fa77581439bbeee19bd4608216b6c3abed355da89bdaa678b0db0b5009c679953b39c49c294e93737ac2849f2fdde6b7d54ef6c", + "lib\/AppInfo\/Application.php": "e7330bfee8a212a8c8d88d2091f02fea8f051de200c5ec54ddc448b12bfff3b8af0b07b4f2b7bffba1975eeb24691d0b4135eba7bc81c5412013df9d5380b255", + "lib\/BackendUtility.php": "ece4949e04c0a86c808a0e8eb57310dc455ff1ce328a63a54ea253822785e279e0136c65054c619c5fe5229be5b37776cb04b33d83712e129830e94939145c60", + "lib\/Command\/CheckUser.php": "1860e745c85c1ed81efd717c1d726a73f1c5c663c77f80678cc8d2194e001ffb9415be45fec168cc8fb80e886236ee96c5cc90a401c4bc8547503ea0c083719e", + "lib\/Command\/CreateEmptyConfig.php": "0f32781a5ce2cbf2d7366eb2a0ac5b2297a067053b4b451c3b72c38d314870b08ce18d52f7de85185aa30b41ae8594059300c95f6fdfa4216ace07011a169ec6", + "lib\/Command\/DeleteConfig.php": "732bafdf5965e52a76c51b2f6f782247c2211a962e6d89987faf469012f092d6e5582d169b88805d105643b8a036af5d1202f71cd161ef1eeeb27aedd019a492", + "lib\/Command\/ResetUser.php": "155625d1739a3770730b8b24e4b242086809134bbd01e98d8b28becce5c93ac453d5de7c7762fa0e38dfde32da08d009db4009102fc23f06711e088ea6db2ad6", + "lib\/Command\/Search.php": "a51e2224a07b7d8e6b7ec4d737282ad87fd1a2a49f5b637d20fa78e9ee1b2476a05a012f41bf7b7b5d1cb7df43a80573ffafec0b4e3681b041ef1b4c30eebc91", + "lib\/Command\/SetConfig.php": "9f4f265862ef4699e899109f12389e89c07921e8048731841e8421fe83b9d98fd9cdfd1f828111740d33cd7e8df0887f349d832c002bd05e56a041ac7d509452", + "lib\/Command\/ShowConfig.php": "2fb6fe87b86d647d7ee0bdaa6ae24e3379ca984061adf691209912a4c876ab672d57897c70224f8975fcef964a3e3d680dab3b9fc444d91fab084abd44b7192f", + "lib\/Command\/ShowRemnants.php": "4127649c38cc74b28fc29f74aa77504913c3db2b1f415c48d9d76145e37acf27e5bfcb2feb8c067ca7131870078c63a08ce0c4e63e6c66ccb680f33beb2960c7", + "lib\/Command\/TestConfig.php": "3ebcda6c52dedceee00ef35175d497537dfad452b5c1dbda7ed19167c1457fa12b41a22ce8fa4d8e199329e8f1e4ddb3e2b1dffabbd812a34040f3587c019b2e", + "lib\/Command\/UpdateUUID.php": "3234b47b8d92f300a7b0aadc4622e2939c0eb20e0f7f1e6b71061381298aae9c6c6041b57ff84f0820c54a6cb14e3640701f62b1b19252685bfe8973e0779b4f", + "lib\/Configuration.php": "62f1c90fec0ac3da2615a4ab29aa99c777f4483402413eda3696638d32ea397a6aff3e843d357c8c75b0f555820f761305ed1f9942a24c9a42c1beb9726db281", + "lib\/Connection.php": "6561803443e738218a3e28e5b960f3599c03572da404703cbe9db52824ff63938db5fc2d6e3c0da5463cd416613596f9ddd51bc2c29a02f88b077831b9bd64a6", + "lib\/ConnectionFactory.php": "e1840769d3f4182f972ee96ddf28868c692596b5cd4ecaee314ee0a1b0916770c1861d7e18260accd1f755a0da680209ffa7638c76659890c50028e5a3ad4d83", + "lib\/Controller\/ConfigAPIController.php": "0839540a40ad86b9b73f58ea2892b5155bf35a7f7e1edcc20f3d199adebad313bd8c774cf9eaa20bbf8bcf18e998827e587682bd9d49d3a3c59b996deb26f144", + "lib\/Controller\/RenewPasswordController.php": "2945e3ac2eac72700c20f673fd05699b09b2c3c3247672aded135d491e6feb0d0843bd13c232eb63d5c79a25436c87b9fdaef3a31d0a37465566ede670a49519", + "lib\/Events\/GroupBackendRegistered.php": "d200667e83f1ef42fd490f9020785fa7ad93112103e48155c567293bce3fffb19ba190af63f512322ea79f8d905ea8ebbdf74e38a49ec1706e4202836541055a", + "lib\/Events\/UserBackendRegistered.php": "f31c3ace2fca15c0fc21f0b6991d3b65aeeec634dd4d046a7d2e8a592f52100d543b0a571484f6b60908ba814698a530ae2c57caff73bae54beb39ec9502370c", + "lib\/Exceptions\/AttributeNotSet.php": "18e6e419beda48951ceb8861c7f6c9c0328ea1eea653c53239d0f39f6961f14e7c30bf45ba8bf9226ad6cd8b6634571f50a74e06af93a741082048087f5e3de5", + "lib\/Exceptions\/ConstraintViolationException.php": "72fd39d5be57c5a48888bf472a0739a0f8d6cd576938e59bf9cd806d8328dae89e336992b2f449c3b034d37d5fc1562a558eded6048f84845925393025438388", + "lib\/Exceptions\/NoMoreResults.php": "ecce4d8d0ed3eac00cd4a2937c80d167f312017f7025325bf87e1bc963f4bdadf002698f4f615b750066dc9684bb958b0ce17622d1970f6da6f999bbd19c56d2", + "lib\/Exceptions\/NotOnLDAP.php": "4d34954246a2c4c76bdd500054d2f26fee87939c80e655da9f7979465e9b13822fa554ce2bf2ed345a6033739aa2a380dcf168bbbcbf2ca316d8ba4e09ad2ba0", + "lib\/FilesystemHelper.php": "a6a14d047a8df84727524d1bfa8e7209f3e12a971c44a26ff2603679b669956ac0e4d8f41f14d5d863db92e767c614f5403ca9548509285d2d04acbc5e36d185", + "lib\/GroupPluginManager.php": "0423c73153dd13faf2d64631faccf4451377f9c3b9fcbaad9cec689bf5b555559ed806c3ca3419a1369bd19534ef1b1370cd5c40e792fcf337f2063822a1e179", + "lib\/Group_LDAP.php": "62312f1063b3bd97f6d3a53b4f55b8b1c51f342123b8830c550798f2afa8e71a157fa86df25e9767dbdec3046f3dde8a6d919beb59ef973d67148a01e7123307", + "lib\/Group_Proxy.php": "def097ae96ee7c14991961071735e005c15d0721d4a4c94dbe7699deb5a64e75eb3a767f0b22a3bcdd17ea186614673bd3fc9fbcbcc39d482f085bfbe943e8cb", + "lib\/Handler\/ExtStorageConfigHandler.php": "ae2f5aa197de97420beb43c51abf4d1bc5bbbc893078bce95c4b65c32b8c5c4f76aaacc756d80ecec51a8744933bb3c573abea125c0f96b72116172c3415725b", + "lib\/Helper.php": "bf6561094b91db5fff8215612b1882bc50a6e04f92c2a4f7e83dc4e04ebc12c4665630c58e08b9c49954e30f6488fb073d25548209d9670eb76050975c7e4b00", + "lib\/IGroupLDAP.php": "c654e34a049c5ef08477b82cb4b169f07842cdc30f11eab967a0a8521429937cfa63a2ed70c4bd24ffbe70bdfee9c536f9e1a6c07627153b7b81db473795dad8", + "lib\/ILDAPGroupPlugin.php": "feafa3ce79a81618d5b618c2a5018a6357df7d584456ac9664b3f8d6607c1c71571d7cc3499ac766519c202a81346021792d695a7d8643d04e87ada4de97eb34", + "lib\/ILDAPUserPlugin.php": "af9d04d6bca0ff2cabc8219badea1b65b3142fa557cf5808d84ad5a79dc26680cee38bcfcca2217aad9c10febe75b2e79e5ef2e3cf29133dad055bafbf3890c8", + "lib\/ILDAPWrapper.php": "ff9784916e0c8da8dc2e655c7e93d25be0ca2086b4ce231b0ec2769f555370d30d0b08158f2c46c5d8542ac42e0b5f7aedfe59d027c1a6cdb9a05244746e6578", + "lib\/IUserLDAP.php": "c507197065791659755e69a1c9ce6d0d8f8e243b53a8409046fb172b270db33c61f0fd760b7e548ca0f2fbf23d747686f05ce95fd2ee59bd3a512a12f90df3ab", + "lib\/Jobs\/CleanUp.php": "9c613ad5793d0718bd0416d1d3e75f0255cf7952499e8197bd06a2d94a44a0072ef8ab98a5317d9af77f77c6f35f3c780de73fdfb25c487d15467a2664987fe9", + "lib\/Jobs\/Sync.php": "ca0aee704feaee001f2f7bc2975d6364c2b689c92fe33c602c36ff342ba9d0d269943cd2ce67c2dd8f47151b4e2c509c546fc9ddd86024143451f608eea5162a", + "lib\/Jobs\/UpdateGroups.php": "61ba97c7618cfbd5988e0b9c8e1ba6cb8051a0300a33a5dc5755c0f81cde183a31579cd862e9db449f371341fe1d51db8d2c1df96a2b2d0851c1790c47d7c181", + "lib\/LDAP.php": "abf403acf12b49b82803b090a0ac951b33585371ef2209d5390fde0c48a3322eadcd6a4f43f4cefe33dec9bc098e2796807f99789819aabb52797f594b4028c4", + "lib\/LDAPProvider.php": "379cb30e51f7e82e181126f582c811527486c539c3cb107295c97ad822c5d27a17535e8e2c819361c2cae99e2e9f52036988829196339286a16a23e0a9719dcf", + "lib\/LDAPProviderFactory.php": "659e3368302fd22a49ae257c446c9bf77235c3df3dd060af2ce5744400bd856bca0743aff29cbbe1b2da35f66d54edcb592646551a042906c2584b4286dc6361", + "lib\/LDAPUtility.php": "eb057d0ef0b5c499fb8235cf2ace9ff3458ea2b1692e093839df2aa6a18a1e62f970fa313bf425d6080e217b27e016d087d8fed51aaa1884baa9cd1aee69e49e", + "lib\/LogWrapper.php": "cf4a777e4f6252e7d754e2c36afeee565dbd8ddc9c467475ef608561509b98344cba416f77e1c78d6f9ab2841ecd38d2d3b51e3d2707cc247b9a66e99b0ca705", + "lib\/Mapping\/AbstractMapping.php": "31061d98f6b9a200740f62c98937ea0b04d35b0522db656fa8005321cb2c22d44727bdade91bdb688079d85ac318fdf0c79250a24788551f66386365816b172b", + "lib\/Mapping\/GroupMapping.php": "fa8e51cdd7f69f39271b0e6e49eaa84bb051a4e4afe1e758454f82273fc328a6bf41f578e8282eb5a906924df6bf973a83a9daaef048bbba27d878842bb7d2e5", + "lib\/Mapping\/UserMapping.php": "4da1e822631d0b80a1a0415a0d64b94ab9e76b304bd1176b5757c90eee1ba95b791a2fd018d5eaa1d59bdd6f2bce596f94aba8642d27e1ed2ed970b3054d6e57", + "lib\/Migration\/GroupMappingMigration.php": "864ec9f8d23d38525ba481f42400ed6a8b5776b11a9627667fc1e35ac56e805c84bda525fdce59d99290e45743214a7e61c6de74c1170f67721ec8bfc0d7589b", + "lib\/Migration\/RemoveRefreshTime.php": "bc3bd50145fb5157466f555ad4782918378a13715e429408992506d3d00da2a5eb6c7d93ed43f8318b55691632864787a049f0038a11b9f7c7d5d49c05df1fb4", + "lib\/Migration\/UUIDFix.php": "f842e8c24a89af8dad67f98a9aafc5dae7e6a818cfd5d7f9cc67976b0ae5833bdfb54d808b8af20e419c427bc39e93e77153e039d6421fd18637e9a80394ad79", + "lib\/Migration\/UUIDFixGroup.php": "cdf941da5e238e0e6d89f05fcce9bc8c9b5779c13c0aa5c46f9fc73ab62882cb349714f03d41f6ddbf9faef443f491c3fcb26fbe49d4ed680159aa59c22c2ef8", + "lib\/Migration\/UUIDFixInsert.php": "998f6b89bbf1dccf9f798a514f66d788e1a387ecb10ff0448b2b07f61d4b8aa5965ceb688769c911926d4aba3649bc2dd5e6b0ccbb2478c8893dd3f5184b5bdc", + "lib\/Migration\/UUIDFixUser.php": "b339f54f7948b730f23aa68c445f68e76a07685c613d25c0a2e03dc59465b05d063115ab4505174d46c92b5773bc8f4395e439f128b36bcc4dbf4e4e86bffffa", + "lib\/Migration\/Version1010Date20200630192842.php": "d4f1ba48d37028dd3f5220113010c8e6bdacbe3b11455098620d2332fd3b7bca1b6b8fca573491f355c4e8eaf5d55dcc42a08ead36b2ccb69daac91162ddb9dc", + "lib\/Migration\/Version1120Date20210917155206.php": "e449c7f9b6ed215d8513c1a1ee835c3634fc46bf5be79f41f8b6f9a861f59f34058897f560463e5ddb717cc93af8b38306ea7a9b45786f259fba18942d2f36ec", + "lib\/Migration\/Version1130Date20211102154716.php": "2f00d2050ed5add785cd0744260f0a84bf12197b2dff6cd6beffcf76ed12a862cd4bcfcbdfa73a25bee1db905b90f1ea5f13365331c9a9463e9bce32773b2e9f", + "lib\/Migration\/Version1130Date20220110154717.php": "661fef74c01bad1877016ebd09932ceaf4bb4d03012b6066249ba3ae4afa8ba4a3f88727a55ddccd9cd1ad516ba10e43a6de2677f9653089335d613709560b44", + "lib\/Migration\/Version1130Date20220110154718.php": "8b3bc67bbe23148cb3a0f9ab1ffccfd6d6e8ed62ffb35119e34263843dd817c7a0fad9bb88e15a1e87a839f5a1239310588c5ac4e05799b5b58f673849df540b", + "lib\/Migration\/Version1130Date20220110154719.php": "2e10d547abd51381e50245e6d0f3687b7b35d8b9a0d37e2e135e63728e4bed4427bf228df96672904661c42dcf2794cb7e476b2d4cc380b054d3303381a4a799", + "lib\/Notification\/Notifier.php": "ae864be078a1d84744ce31c087fe3014ebe7ea77bb0544d44cd8bce6ed96bfbccd0ea34d90bb609d147cc41d60116a6f5aa6284c9553ac2ba496b2fad5169819", + "lib\/PagedResults\/IAdapter.php": "b8d14dc83b72ee3ea1d5faec4fbd389757ae7275e98430235c9334f83a9020480cff7babbbb66f207813a5d591d74c5bbee0c92b0a03d67e7dcba307e9f8ffd1", + "lib\/PagedResults\/Php54.php": "cbdc343d41c46b56d35ada91b217431f8477a4ee129a01e22a4c283452e574620aa892401ea509b685dff9589d73debdb899ff939dacb9b0944da6ff07391ecb", + "lib\/PagedResults\/Php73.php": "0bfbb04a8e6f987cc68f252a343fc7ac2d7729af9b00606959caebb22e8f1e59ccb18992ab3f20182688717f054ab80bc1c05e86ad454221e7a458ac91465092", + "lib\/PagedResults\/TLinkId.php": "1a624b177c9570a6ec65eb8524860b352c9f3fed50529ba875c428b475bc1d572d14b3128370576038efd190587c1407fb8d6890cb54e2d743ce9ed82514ccf5", + "lib\/Proxy.php": "6c330b78a87b370464e6477d3d927bedcf3273605360047b1175704d0073a8ab2297e73ebeda095d272d5828dc7de92ffb1e20847d98039187580faab1d1b06b", + "lib\/Settings\/Admin.php": "9183621719c8a680cd4d59b3629208c02651339d69f7ae01e5a2c94a4f51b97126d89b1db04f6b1a9e3c8454bdc581879bd0b7b56316b3f078628eab50909e2e", + "lib\/Settings\/Section.php": "d10b6c84c99acacd160b1a869d9155eee3d935d8a5340d1d705ca2a692f24fbeb80ce323f1e15e47f5cb196388e74c5fd3e9805aea039612602518504a824be9", + "lib\/User\/DeletedUsersIndex.php": "73dc5264212572e146beb3c3028bc81577d3524e43b0927eb9e4d9aa50f52fa7268e613b0336f3c6c5fcef0bd73019d3a73bd9f60aafe105e25e2774b8320681", + "lib\/User\/Manager.php": "88b440109bb8ecf1eeacf09d3cfe5deafffdb0d489a1bad02553748630282f8ba48bcb44ad9086ad22e2497e58eeda02aad6f6144371ca0a5627da9c98ed2322", + "lib\/User\/OfflineUser.php": "4a50095c305aa1f24af2c98cd882fd801d6d69fcb12d88c13d6f3c72c59f2e7c257e20b1f0c7af6a0658e3f2ca42bbbe19816d38a0f804db6e0ef1534294e492", + "lib\/User\/User.php": "3a9175eb11905321d8bfd0900df11177695dfa15c54288d75694fd7946e76353eca69668a263a5e875c7504eba5ed97ef9bf3f530d58c5f585ae98eb23b1fc08", + "lib\/UserPluginManager.php": "8a9f4605a04a9eeb6d9fc1d5cc0986387fa3f6e0b51d2160fe332cdb1515c776ed17700fec7759d1ca81f1f41bccc82ecc5f505f5aca86b8940ab3406e09df66", + "lib\/User_LDAP.php": "506a3ad516bd9528829491505beb9645f1cbef14002129266706d7c9b90dc43709ab9b67e49a15cc68bef1c99c27a5ed6eecaec344f5993c04dc452086643cb8", + "lib\/User_Proxy.php": "3792bf686a54baf7e43f6a066e172d1336a178e2daa5fce11dc9a716f022f224e65ecac1924cf717fd8dd168bcd7cbc7ef10b3aa5052bd796876040b92dc16c6", + "lib\/Wizard.php": "4dd4bed76ee232274403ed43be2d9ccd98dafb22372e074cd00eee28ef9102de30710a0500bbc2e094b2501b6ee3b73923be893fbfd6f33fae6f38e3c45ebdd5", + "lib\/WizardResult.php": "7acfbd78c5d5c155dbefbd73fb9f7ec0ffcf083d176dc408681600367e5f4d671386cc4eabf20a3f4f25d462c893f8f74992aad2f61cf9c75dc056e039eced6b", + "templates\/part.settingcontrols.php": "06779143ed2405be1010f4383ef40f7130ec8ec5ecb84ca2c4633a7d2b38a59bd2c0c8696b181b63d4d254b4772808e5eea802f857c192b389be4189b7a9e9e7", + "templates\/part.wizard-groupfilter.php": "fd0c2fe34d0e4db938fd9a1955f72a906576ac16d3fedced2cdc3c549d8a7299e093ab1dad9ab419731fe6d4aa9b878d8153c7dc0a050014418ee63150997cfc", + "templates\/part.wizard-loginfilter.php": "ff5ee3ea77bc0b11f1ed0015029574c857872bf86dbf348bde6989597a8dcc1deb0c871c1ba73242688e13107f6002f1d1d2480190ae18c11f3a11931e4918d2", + "templates\/part.wizard-server.php": "9f04e596ca47ea57e54871bfb13b3cc4a6120d2011ffae6c0d300d9f4698427ef0c2d6b89e43a24b954f2603676277da9566d6b0eaca87fc97f47bc4928db5d7", + "templates\/part.wizard-userfilter.php": "cdc24b922c03685ebe9ae5a4e6301831fcd6403eab1cec3c037ba407bcbc6ba3679bfe1380530c6294f127603fb1f8d99746b431a120798f32b307a96b4a4a00", + "templates\/part.wizardcontrols.php": "6f81943c9ecc3b1f5cee936e433d9d8e50a48b5b30de46e4f60c62914415e51cb3edd165b0cfa8eabcab17e262c245c22c5921672c4e10f119725c08b05725fe", + "templates\/renewpassword.php": "ad655ba0756465ceda08e3614b1c29f1c7461c58182fd60e1f36044d77ceed06e8b60d5510e099dcfcdc05540fbc5cb539f756ca38342736a8acd0ae0cf4ec32", + "templates\/settings.php": "aa03548ff192a4018940f022e55d348085a529b77af92f62ccd02fba55014b284c0039fe012714e549e8def86d3b9d6a826eef23413de87df876f22ff156e241", + "vendor\/ui-multiselect\/MIT-LICENSE": "b27f1e9fc12aa2f9fb5cf3db9f24141f158e82770e8a00fe0ac464478ce9803d2db7d3e5257cf6597adf6c2c3a2b28c0adce530f5a72698a754b0a0983bf8c1c", + "vendor\/ui-multiselect\/jquery.multiselect.css": "cb29f588e4aa9f3d460315384940abc481b7121fe18ed56077e8e5608dc4dc01df1083c76871ae7109228a3b1a0c169962fb901bba323d91f2f4e5e2c16dff5e", + "vendor\/ui-multiselect\/src\/jquery.multiselect.js": "8a30cdb626fe10b5a93ad61bd2eeb39c0628ee1bdeb9fc7bb1761a1bb972c9768a23c6a24e613167ccd49b93a50688cefe22001daa9627e89a1055b1d56cf700" + }, + "signature": "dhQQqqGkK9tVsT+5DowZgppLeuzZT7gWoDoo7cMxG+oiflsWxlPClXu54Lh6or2tHK2lzEePz+fsjF3n6u2LmmbDRJ6bR6vKzWu9PZAkQEqW+SZn7VmRNiFYb3pBdVuCLqfBcKkWco2YOed5modGnjiuc7WatIovrY3VoqMERLJgzKUJNurCj1wyuD2zMOiL6IbnR6PpMRVmbw2WYC46DstRHwug0rCx1MmODIcmheQ0nR0VIZ0N+NY\/s1xAJCo\/n+osa0qPcFXqJ\/ba\/PL\/erifiX1I4e+T7lQBoQjf5k9IDeyNtb61AsjxjrWDLtA+c3EulkdrozLEhMSaXHTYDw==", + "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEojCCA4qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwezELMAkGA1UEBhMCREUx\r\nGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzEXMBUGA1UECgwOTmV4dGNsb3Vk\r\nIEdtYkgxNjA0BgNVBAMMLU5leHRjbG91ZCBDb2RlIFNpZ25pbmcgSW50ZXJtZWRp\r\nYXRlIEF1dGhvcml0eTAeFw0xNjA2MTIyMTA1MDZaFw00MTA2MDYyMTA1MDZaMGYx\r\nCzAJBgNVBAYTAkRFMRswGQYDVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxEjAQBgNV\r\nBAcMCVN0dXR0Z2FydDEXMBUGA1UECgwOTmV4dGNsb3VkIEdtYkgxDTALBgNVBAMM\r\nBGNvcmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUxcrn2DC892IX\r\n8+dJjZVh9YeHF65n2ha886oeAizOuHBdWBfzqt+GoUYTOjqZF93HZMcwy0P+xyCf\r\nQqak5Ke9dybN06RXUuGP45k9UYBp03qzlUzCDalrkj+Jd30LqcSC1sjRTsfuhc+u\r\nvH1IBuBnf7SMUJUcoEffbmmpAPlEcLHxlUGlGnz0q1e8UFzjbEFj3JucMO4ys35F\r\nqZS4dhvCngQhRW3DaMlQLXEUL9k3kFV+BzlkPzVZEtSmk4HJujFCnZj1vMcjQBg\/\r\nBqq1HCmUB6tulnGcxUzt\/Z\/oSIgnuGyENeke077W3EyryINL7EIyD4Xp7sxLizTM\r\nFCFCjjH1AgMBAAGjggFDMIIBPzAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIG\r\nQDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgU2VydmVyIENlcnRp\r\nZmljYXRlMB0GA1UdDgQWBBQwc1H9AL8pRlW2e5SLCfPPqtqc0DCBpQYDVR0jBIGd\r\nMIGagBRt6m6qqTcsPIktFz79Ru7DnnjtdKF+pHwwejELMAkGA1UEBhMCREUxGzAZ\r\nBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzESMBAGA1UEBwwJU3R1dHRnYXJ0MRcw\r\nFQYDVQQKDA5OZXh0Y2xvdWQgR21iSDEhMB8GA1UEAwwYTmV4dGNsb3VkIFJvb3Qg\r\nQXV0aG9yaXR5ggIQADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUH\r\nAwEwDQYJKoZIhvcNAQELBQADggEBADZ6+HV\/+0NEH3nahTBFxO6nKyR\/VWigACH0\r\nnaV0ecTcoQwDjKDNNFr+4S1WlHdwITlnNabC7v9rZ\/6QvbkrOTuO9fOR6azp1EwW\r\n2pixWqj0Sb9\/dSIVRpSq+jpBE6JAiX44dSR7zoBxRB8DgVO2Afy0s80xEpr5JAzb\r\nNYuPS7M5UHdAv2dr16fDcDIvn+vk92KpNh1NTeZFjBbRVQ9DXrgkRGW34TK8uSLI\r\nYG6jnfJ6eJgTaO431ywWPXNg1mUMaT\/+QBOgB299QVCKQU+lcZWptQt+RdsJUm46\r\nNY\/nARy4Oi4uOe88SuWITj9KhrFmEvrUlgM8FvoXA1ldrR7KiEg=\r\n-----END CERTIFICATE-----" +} \ No newline at end of file diff --git a/apps/user_ldap/l10n/.gitkeep b/apps/user_ldap/l10n/.gitkeep deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index b96cb25e631bc..ea3176f7a77d6 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -527,6 +527,11 @@ public function dn2username($fdn, $ldapName = null) { * @throws \Exception */ public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) { + static $intermediates = []; + if (isset($intermediates[($isUser ? 'user-' : 'group-') . $fdn])) { + return false; // is a known intermediate + } + $newlyMapped = false; if ($isUser) { $mapper = $this->getUserMapper(); @@ -562,6 +567,7 @@ public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped $ldapName = $this->readAttribute($fdn, $nameAttribute, $filter); if (!isset($ldapName[0]) || empty($ldapName[0])) { $this->logger->debug('No or empty name for ' . $fdn . ' with filter ' . $filter . '.', ['app' => 'user_ldap']); + $intermediates[($isUser ? 'user-' : 'group-') . $fdn] = true; return false; } $ldapName = $ldapName[0]; diff --git a/apps/user_ldap/lib/Configuration.php b/apps/user_ldap/lib/Configuration.php index 5a7c732ab7b54..835211efaa268 100644 --- a/apps/user_ldap/lib/Configuration.php +++ b/apps/user_ldap/lib/Configuration.php @@ -63,6 +63,8 @@ class Configuration { 'ldapPort' => null, 'ldapBackupHost' => null, 'ldapBackupPort' => null, + 'ldapBackgroundHost' => null, + 'ldapBackgroundPort' => null, 'ldapBase' => null, 'ldapBaseUsers' => null, 'ldapBaseGroups' => null, @@ -437,6 +439,8 @@ public function getDefaults() { 'ldap_port' => '', 'ldap_backup_host' => '', 'ldap_backup_port' => '', + 'ldap_background_host' => '', + 'ldap_background_port' => '', 'ldap_override_main_server' => '', 'ldap_dn' => '', 'ldap_agent_password' => '', @@ -501,6 +505,8 @@ public function getConfigTranslationArray() { 'ldap_port' => 'ldapPort', 'ldap_backup_host' => 'ldapBackupHost', 'ldap_backup_port' => 'ldapBackupPort', + 'ldap_background_host' => 'ldapBackgroundHost', + 'ldap_background_port' => 'ldapBackgroundPort', 'ldap_override_main_server' => 'ldapOverrideMainServer', 'ldap_dn' => 'ldapAgentName', 'ldap_agent_password' => 'ldapAgentPassword', diff --git a/apps/user_ldap/lib/Connection.php b/apps/user_ldap/lib/Connection.php index 63f2296a16723..c48ff2ff97358 100644 --- a/apps/user_ldap/lib/Connection.php +++ b/apps/user_ldap/lib/Connection.php @@ -267,7 +267,7 @@ public function getFromCache($key) { * * @return string */ - public function writeToCache($key, $value) { + public function writeToCache($key, $value, int $ttlOverride = null) { if (!$this->configured) { $this->readConfiguration(); } @@ -278,7 +278,8 @@ public function writeToCache($key, $value) { } $key = $this->getCacheKey($key); $value = base64_encode(json_encode($value)); - $this->cache->set($key, $value, $this->configuration->ldapCacheTTL); + $ttl = $ttlOverride ?? $this->configuration->ldapCacheTTL; + $this->cache->set($key, $value, $ttl); } public function clearCache() { @@ -570,18 +571,32 @@ private function establishConnection() { $isOverrideMainServer = ($this->configuration->ldapOverrideMainServer || $this->getFromCache('overrideMainServer')); - $isBackupHost = (trim($this->configuration->ldapBackupHost) !== ""); + $isBackupHost = (trim($this->configuration->ldapBackupHost) !== "") + && (!\OC::$CLI || !$this->configuration->ldapBackgroundHost); $bindStatus = false; + $mainServerConnectionFailure = false; try { if (!$isOverrideMainServer) { - $this->doConnect($this->configuration->ldapHost, - $this->configuration->ldapPort); + $host = $this->configuration->ldapHost; + $port = $this->configuration->ldapPort; + if (\OC::$CLI && $this->configuration->ldapBackgroundHost) { + $host = $this->configuration->ldapBackgroundHost; + $port = $this->configuration->ldapBackgroundPort; + } + $this->doConnect($host, $port); return $this->bind(); } } catch (ServerNotAvailableException $e) { if (!$isBackupHost) { throw $e; } + $mainServerConnectionFailure = true; + $this->logger->info( + 'Main LDAP not reachable, connecting to backup', + [ + 'app' => 'user_ldap' + ] + ); } //if LDAP server is not reachable, try the Backup (Replica!) Server @@ -592,10 +607,10 @@ private function establishConnection() { $bindStatus = $this->bind(); $error = $this->ldap->isResource($this->ldapConnectionRes) ? $this->ldap->errno($this->ldapConnectionRes) : -1; - if ($bindStatus && $error === 0 && !$this->getFromCache('overrideMainServer')) { + if ($bindStatus && $error === 0 && $mainServerConnectionFailure && !$this->getFromCache('overrideMainServer')) { //when bind to backup server succeeded and failed to main server, - //skip contacting him until next cache refresh - $this->writeToCache('overrideMainServer', true); + //skip contacting it for 15min + $this->writeToCache('overrideMainServer', true, 60 * 15); } } @@ -625,6 +640,10 @@ private function doConnect($host, $port) { throw new ServerNotAvailableException('Could not disable LDAP referrals.'); } + if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_NETWORK_TIMEOUT, 8)) { + throw new ServerNotAvailableException('Could not set network timeout'); + } + if ($this->configuration->ldapTLS) { if (!$this->ldap->startTls($this->ldapConnectionRes)) { throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.'); diff --git a/apps/user_ldap/lib/Group_LDAP.php b/apps/user_ldap/lib/Group_LDAP.php index 8bf13ed90fd63..8b21b9348821f 100644 --- a/apps/user_ldap/lib/Group_LDAP.php +++ b/apps/user_ldap/lib/Group_LDAP.php @@ -57,7 +57,7 @@ class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, I /** @var string[][] $cachedGroupMembers array of users with gid as key */ protected $cachedGroupMembers; - /** @var string[] $cachedGroupsByMember array of groups with uid as key */ + /** @var string[][] $cachedGroupsByMember array of groups with uid as key */ protected $cachedGroupsByMember; /** @var string[] $cachedNestedGroups array of groups with gid (DN) as key */ protected $cachedNestedGroups; @@ -88,7 +88,7 @@ public function __construct(Access $access, GroupPluginManager $groupPluginManag } /** - * is user in group? + * Check if user is in group * * @param string $uid uid of the user * @param string $gid gid of the group @@ -240,18 +240,21 @@ public function getDynamicGroupMembers(string $dnGroup): array { } /** + * Get group members from dn. + * @psalm-param array $seen List of DN that have already been processed. * @throws ServerNotAvailableException */ private function _groupMembers(string $dnGroup, ?array &$seen = null): array { if ($seen === null) { $seen = []; - // the root entry has to be marked as processed to avoind infinit loops, - // but not included in the results laters on + // the root entry has to be marked as processed to avoid infinite loops, + // but not included in the results later on $excludeFromResult = $dnGroup; } // cache only base groups, otherwise groups get additional unwarranted members $shouldCacheResult = count($seen) === 0; + /** @psalm-var array $rawMemberReads */ static $rawMemberReads = []; // runtime cache for intermediate ldap read results $allMembers = []; @@ -302,10 +305,10 @@ private function _groupMembers(string $dnGroup, ?array &$seen = null): array { $rawMemberReads[$dnGroup] = $members; } if (is_array($members)) { - $fetcher = function ($memberDN) use (&$seen) { + $fetcher = function (string $memberDN) use (&$seen) { return $this->_groupMembers($memberDN, $seen); }; - $allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members, $seen); + $allMembers = $this->walkNestedGroupsReturnDNs($dnGroup, $fetcher, $members, $seen); } $allMembers += $this->getDynamicGroupMembers($dnGroup); @@ -331,6 +334,7 @@ private function _groupMembers(string $dnGroup, ?array &$seen = null): array { } /** + * @return string[] * @throws ServerNotAvailableException */ private function _getGroupDNsFromMemberOf(string $dn): array { @@ -339,7 +343,7 @@ private function _getGroupDNsFromMemberOf(string $dn): array { return []; } - $fetcher = function ($groupDN) { + $fetcher = function (string $groupDN) { if (isset($this->cachedNestedGroups[$groupDN])) { $nestedGroups = $this->cachedNestedGroups[$groupDN]; } else { @@ -352,50 +356,80 @@ private function _getGroupDNsFromMemberOf(string $dn): array { return $nestedGroups; }; - $groups = $this->walkNestedGroups($dn, $fetcher, $groups); + $groups = $this->walkNestedGroupsReturnDNs($dn, $fetcher, $groups); return $this->filterValidGroups($groups); } - private function walkNestedGroups(string $dn, Closure $fetcher, array $list, array &$seen = []): array { - $nesting = (int)$this->access->connection->ldapNestedGroups; - // depending on the input, we either have a list of DNs or a list of LDAP records - // also, the output expects either DNs or records. Testing the first element should suffice. - $recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]); - - if ($nesting !== 1) { - if ($recordMode) { - // the keys are numeric, but should hold the DN - return array_reduce($list, function ($transformed, $record) use ($dn) { - if ($record['dn'][0] != $dn) { - $transformed[$record['dn'][0]] = $record; - } - return $transformed; - }, []); - } - return $list; - } - + /** + * @psalm-param list}|string> $list + * @psalm-param array $seen List of DN that have already been processed. + * @param Closure(string) $fetcher + */ + private function processListFromWalkingNestedGroups(array &$list, array &$seen, string $dn, Closure $fetcher): void { while ($record = array_shift($list)) { $recordDN = $record['dn'][0] ?? $record; if ($recordDN === $dn || array_key_exists($recordDN, $seen)) { // Prevent loops continue; } - $fetched = $fetcher($record); + + $cacheKey = 'walkNestedGroups_' . $recordDN; + $fetched = $this->access->connection->getFromCache($cacheKey); + if ($fetched === null) { + $fetched = $fetcher($recordDN); + $this->access->connection->writeToCache($cacheKey, $fetched); + } $list = array_merge($list, $fetched); if (!isset($seen[$recordDN]) || is_bool($seen[$recordDN]) && is_array($record)) { $seen[$recordDN] = $record; } } + } + + /** + * @psalm-param list}|string> $list + * @psalm-param array $seen List of DN that have already been processed. + * @param Closure(string) $fetcher + */ + private function walkNestedGroupsReturnDNs(string $dn, Closure $fetcher, array $list, array &$seen = []): array { + $nesting = (int)$this->access->connection->ldapNestedGroups; + + if ($nesting !== 1) { + return $list; + } - // on record mode, filter out intermediate state - return $recordMode ? array_filter($seen, 'is_array') : array_keys($seen); + $this->processListFromWalkingNestedGroups($list, $seen, $dn, $fetcher); + return array_keys($seen); } /** - * translates a gidNumber into an ownCloud internal name + * @psalm-param list}> $list + * @psalm-param array $seen List of DN that have already been processed. + * @return array[] An array of records + * @param Closure(string) $fetcher + */ + private function walkNestedGroupsReturnRecords(string $dn, Closure $fetcher, array $list, array &$seen = []): array { + $nesting = (int)$this->access->connection->ldapNestedGroups; + + if ($nesting !== 1) { + // the keys are numeric, but should hold the DN + return array_reduce($list, function (array $transformed, array $record) use ($dn) { + if ($record['dn'][0] != $dn) { + $transformed[$record['dn'][0]] = $record; + } + return $transformed; + }, []); + } + + $this->processListFromWalkingNestedGroups($list, $seen, $dn, $fetcher); + // filter out intermediate state + return array_filter($seen, 'is_array'); + } + + /** + * Translates a gidNumber into the Nextcloud internal name. * - * @return string|bool + * @return string|false The nextcloud internal name. * @throws Exception * @throws ServerNotAvailableException */ @@ -416,10 +450,11 @@ public function gidNumber2Name(string $gid, string $dn) { } /** + * @return ?string The name of the group * @throws ServerNotAvailableException * @throws Exception */ - private function getNameOfGroup(string $filter, string $cacheKey) { + private function getNameOfGroup(string $filter, string $cacheKey): ?string { $result = $this->access->searchGroups($filter, ['dn'], 1); if (empty($result)) { $this->access->connection->writeToCache($cacheKey, false); @@ -438,9 +473,7 @@ private function getNameOfGroup(string $filter, string $cacheKey) { } /** - * returns the entry's gidNumber - * - * @return string|bool + * @return string|bool The entry's gidNumber * @throws ServerNotAvailableException */ private function getEntryGidNumber(string $dn, string $attribute) { @@ -452,7 +485,7 @@ private function getEntryGidNumber(string $dn, string $attribute) { } /** - * @return string|bool + * @return string|bool The group's gidNumber * @throws ServerNotAvailableException */ public function getGroupGidNumber(string $dn) { @@ -460,9 +493,7 @@ public function getGroupGidNumber(string $dn) { } /** - * returns the user's gidNumber - * - * @return string|bool + * @return string|bool The user's gidNumber * @throws ServerNotAvailableException */ public function getUserGidNumber(string $dn) { @@ -497,8 +528,7 @@ private function prepareFilterForUsersHasGidNumber(string $groupDN, string $sear } /** - * returns a list of users that have the given group as gid number - * + * @return array A list of users that have the given group as gid number * @throws ServerNotAvailableException */ public function getUsersInGidNumber( @@ -525,7 +555,7 @@ public function getUsersInGidNumber( /** * @throws ServerNotAvailableException - * @return bool + * @return false|string */ public function getUserGroupByGid(string $dn) { $groupID = $this->getUserGidNumber($dn); @@ -540,9 +570,9 @@ public function getUserGroupByGid(string $dn) { } /** - * translates a primary group ID into an Nextcloud internal name + * Translates a primary group ID into an Nextcloud internal name * - * @return string|bool + * @return string|false * @throws Exception * @throws ServerNotAvailableException */ @@ -567,9 +597,7 @@ public function primaryGroupID2Name(string $gid, string $dn) { } /** - * returns the entry's primary group ID - * - * @return string|bool + * @return string|false The entry's group Id * @throws ServerNotAvailableException */ private function getEntryGroupID(string $dn, string $attribute) { @@ -581,7 +609,7 @@ private function getEntryGroupID(string $dn, string $attribute) { } /** - * @return string|bool + * @return string|false The entry's primary group Id * @throws ServerNotAvailableException */ public function getGroupPrimaryGroupID(string $dn) { @@ -589,7 +617,7 @@ public function getGroupPrimaryGroupID(string $dn) { } /** - * @return string|bool + * @return string|false * @throws ServerNotAvailableException */ public function getUserPrimaryGroupIDs(string $dn) { @@ -669,7 +697,7 @@ public function countUsersInPrimaryGroup( } /** - * @return string|bool + * @return string|false * @throws ServerNotAvailableException */ public function getUserPrimaryGroup(string $dn) { @@ -848,6 +876,9 @@ private function getGroupsByMember(string $dn, array &$seen = null): array { // avoid loops return []; } + if ($this->cachedGroupsByMember[$dn]) { + return $this->cachedGroupsByMember[$dn]; + } $allGroups = []; $seen[$dn] = true; $filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn; @@ -865,10 +896,7 @@ private function getGroupsByMember(string $dn, array &$seen = null): array { $groups = $this->access->fetchListOfGroups($filter, [strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']); if (is_array($groups)) { - $fetcher = function ($dn) use (&$seen) { - if (is_array($dn) && isset($dn['dn'][0])) { - $dn = $dn['dn'][0]; - } + $fetcher = function (string $dn) use (&$seen) { return $this->getGroupsByMember($dn, $seen); }; @@ -876,10 +904,12 @@ private function getGroupsByMember(string $dn, array &$seen = null): array { $dn = ""; } - $allGroups = $this->walkNestedGroups($dn, $fetcher, $groups, $seen); + $allGroups = $this->walkNestedGroupsReturnRecords($dn, $fetcher, $groups, $seen); } $visibleGroups = $this->filterValidGroups($allGroups); - return array_intersect_key($allGroups, $visibleGroups); + $effectiveGroups = array_intersect_key($allGroups, $visibleGroups); + $this->cachedGroupsByMember[$dn] = $effectiveGroups; + return $effectiveGroups; } /** @@ -921,7 +951,7 @@ public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) { $groupDN = $this->access->groupname2dn($gid); if (!$groupDN) { - // group couldn't be found, return empty resultset + // group couldn't be found, return empty result-set $this->access->connection->writeToCache($cacheKey, []); return []; } @@ -1185,7 +1215,11 @@ protected function filterValidGroups(array $listOfGroups): array { $validGroupDNs = []; foreach ($listOfGroups as $key => $item) { $dn = is_string($item) ? $item : $item['dn'][0]; - $gid = $this->access->dn2groupname($dn); + if (is_array($item) && !isset($item[$this->access->connection->ldapGroupDisplayName][0])) { + continue; + } + $name = $item[$this->access->connection->ldapGroupDisplayName][0] ?? null; + $gid = $this->access->dn2groupname($dn, $name); if (!$gid) { continue; } @@ -1352,10 +1386,11 @@ public function getDisplayName(string $gid): string { if ($displayName && (count($displayName) > 0)) { $displayName = $displayName[0]; - $this->access->connection->writeToCache($cacheKey, $displayName); - return $displayName; + } else { + $displayName = ''; } - return ''; + $this->access->connection->writeToCache($cacheKey, $displayName); + return $displayName; } } diff --git a/apps/user_ldap/lib/Wizard.php b/apps/user_ldap/lib/Wizard.php index 67a130555f2e9..90c9e9c4323fd 100644 --- a/apps/user_ldap/lib/Wizard.php +++ b/apps/user_ldap/lib/Wizard.php @@ -886,7 +886,7 @@ private function testMemberOf() { throw new \Exception('Could not connect to LDAP'); } $result = $this->access->countUsers('memberOf=*', ['memberOf'], 1); - if (is_int($result) && $result > 0) { + if (is_int($result) && $result > 0) { return true; } return false; diff --git a/apps/user_ldap/tests/.htaccess b/apps/user_ldap/tests/.htaccess deleted file mode 100755 index 6fde30e763a60..0000000000000 --- a/apps/user_ldap/tests/.htaccess +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by ownCloud on 2015-06-18 14:16:40 -# line below if for Apache 2.4 - -Require all denied - - -# line below if for Apache 2.2 - -deny from all -Satisfy All - - -# section for Apache 2.2 and 2.4 - -IndexIgnore * - diff --git a/apps/user_ldap/tests/AccessTest.php b/apps/user_ldap/tests/AccessTest.php deleted file mode 100644 index b97fe321bf70e..0000000000000 --- a/apps/user_ldap/tests/AccessTest.php +++ /dev/null @@ -1,784 +0,0 @@ - - * - * @author Andreas Fischer - * @author Arthur Schiwon - * @author Christoph Wurst - * @author Joas Schilling - * @author Jörn Friedrich Dreyer - * @author Lukas Reschke - * @author Morris Jobke - * @author Roeland Jago Douma - * @author Roger Szabo - * @author root - * @author Thomas Müller - * @author Victor Dubiniuk - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\Access; -use OCA\User_LDAP\Connection; -use OCA\User_LDAP\Exceptions\ConstraintViolationException; -use OCA\User_LDAP\FilesystemHelper; -use OCA\User_LDAP\Helper; -use OCA\User_LDAP\ILDAPWrapper; -use OCA\User_LDAP\LDAP; -use OCA\User_LDAP\LogWrapper; -use OCA\User_LDAP\Mapping\GroupMapping; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\User\Manager; -use OCA\User_LDAP\User\OfflineUser; -use OCA\User_LDAP\User\User; -use OCP\IAvatarManager; -use OCP\IConfig; -use OCP\IDBConnection; -use OCP\Image; -use OCP\IUserManager; -use OCP\Notification\IManager as INotificationManager; -use Psr\Log\LoggerInterface; -use Test\TestCase; - -/** - * Class AccessTest - * - * @group DB - * - * @package OCA\User_LDAP\Tests - */ -class AccessTest extends TestCase { - /** @var UserMapping|\PHPUnit\Framework\MockObject\MockObject */ - protected $userMapper; - /** @var GroupMapping|\PHPUnit\Framework\MockObject\MockObject */ - protected $groupMapper; - /** @var Connection|\PHPUnit\Framework\MockObject\MockObject */ - private $connection; - /** @var LDAP|\PHPUnit\Framework\MockObject\MockObject */ - private $ldap; - /** @var Manager|\PHPUnit\Framework\MockObject\MockObject */ - private $userManager; - /** @var Helper|\PHPUnit\Framework\MockObject\MockObject */ - private $helper; - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - private $config; - /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ - private $ncUserManager; - /** @var LoggerInterface|MockObject */ - private $logger; - /** @var Access */ - private $access; - - protected function setUp(): void { - $this->connection = $this->createMock(Connection::class); - $this->ldap = $this->createMock(LDAP::class); - $this->userManager = $this->createMock(Manager::class); - $this->helper = $this->createMock(Helper::class); - $this->config = $this->createMock(IConfig::class); - $this->userMapper = $this->createMock(UserMapping::class); - $this->groupMapper = $this->createMock(GroupMapping::class); - $this->ncUserManager = $this->createMock(IUserManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->access = new Access( - $this->connection, - $this->ldap, - $this->userManager, - $this->helper, - $this->config, - $this->ncUserManager, - $this->logger - ); - $this->access->setUserMapper($this->userMapper); - $this->access->setGroupMapper($this->groupMapper); - } - - private function getConnectorAndLdapMock() { - $lw = $this->createMock(ILDAPWrapper::class); - $connector = $this->getMockBuilder(Connection::class) - ->setConstructorArgs([$lw, null, null]) - ->getMock(); - $um = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->createMock(IConfig::class), - $this->createMock(FilesystemHelper::class), - $this->createMock(LogWrapper::class), - $this->createMock(IAvatarManager::class), - $this->createMock(Image::class), - $this->createMock(IDBConnection::class), - $this->createMock(IUserManager::class), - $this->createMock(INotificationManager::class)]) - ->getMock(); - $helper = new Helper(\OC::$server->getConfig()); - - return [$lw, $connector, $um, $helper]; - } - - public function testEscapeFilterPartValidChars() { - $input = 'okay'; - $this->assertTrue($input === $this->access->escapeFilterPart($input)); - } - - public function testEscapeFilterPartEscapeWildcard() { - $input = '*'; - $expected = '\\\\*'; - $this->assertTrue($expected === $this->access->escapeFilterPart($input)); - } - - public function testEscapeFilterPartEscapeWildcard2() { - $input = 'foo*bar'; - $expected = 'foo\\\\*bar'; - $this->assertTrue($expected === $this->access->escapeFilterPart($input)); - } - - /** - * @dataProvider convertSID2StrSuccessData - * @param array $sidArray - * @param $sidExpected - */ - public function testConvertSID2StrSuccess(array $sidArray, $sidExpected) { - $sidBinary = implode('', $sidArray); - $this->assertSame($sidExpected, $this->access->convertSID2Str($sidBinary)); - } - - public function convertSID2StrSuccessData() { - return [ - [ - [ - "\x01", - "\x04", - "\x00\x00\x00\x00\x00\x05", - "\x15\x00\x00\x00", - "\xa6\x81\xe5\x0e", - "\x4d\x6c\x6c\x2b", - "\xca\x32\x05\x5f", - ], - 'S-1-5-21-249921958-728525901-1594176202', - ], - [ - [ - "\x01", - "\x02", - "\xFF\xFF\xFF\xFF\xFF\xFF", - "\xFF\xFF\xFF\xFF", - "\xFF\xFF\xFF\xFF", - ], - 'S-1-281474976710655-4294967295-4294967295', - ], - ]; - } - - public function testConvertSID2StrInputError() { - $sidIllegal = 'foobar'; - $sidExpected = ''; - - $this->assertSame($sidExpected, $this->access->convertSID2Str($sidIllegal)); - } - - public function testGetDomainDNFromDNSuccess() { - $inputDN = 'uid=zaphod,cn=foobar,dc=my,dc=server,dc=com'; - $domainDN = 'dc=my,dc=server,dc=com'; - - $this->ldap->expects($this->once()) - ->method('explodeDN') - ->with($inputDN, 0) - ->willReturn(explode(',', $inputDN)); - - $this->assertSame($domainDN, $this->access->getDomainDNFromDN($inputDN)); - } - - public function testGetDomainDNFromDNError() { - $inputDN = 'foobar'; - $expected = ''; - - $this->ldap->expects($this->once()) - ->method('explodeDN') - ->with($inputDN, 0) - ->willReturn(false); - - $this->assertSame($expected, $this->access->getDomainDNFromDN($inputDN)); - } - - public function dnInputDataProvider() { - return [[ - [ - 'input' => 'foo=bar,bar=foo,dc=foobar', - 'interResult' => [ - 'count' => 3, - 0 => 'foo=bar', - 1 => 'bar=foo', - 2 => 'dc=foobar' - ], - 'expectedResult' => true - ], - [ - 'input' => 'foobarbarfoodcfoobar', - 'interResult' => false, - 'expectedResult' => false - ] - ]]; - } - - /** - * @dataProvider dnInputDataProvider - * @param array $case - */ - public function testStringResemblesDN($case) { - list($lw, $con, $um, $helper) = $this->getConnectorAndLdapMock(); - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject $config */ - $config = $this->createMock(IConfig::class); - $access = new Access($con, $lw, $um, $helper, $config, $this->ncUserManager, $this->logger); - - $lw->expects($this->exactly(1)) - ->method('explodeDN') - ->willReturnCallback(function ($dn) use ($case) { - if ($dn === $case['input']) { - return $case['interResult']; - } - return null; - }); - - $this->assertSame($case['expectedResult'], $access->stringResemblesDN($case['input'])); - } - - /** - * @dataProvider dnInputDataProvider - * @param $case - */ - public function testStringResemblesDNLDAPmod($case) { - list(, $con, $um, $helper) = $this->getConnectorAndLdapMock(); - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject $config */ - $config = $this->createMock(IConfig::class); - $lw = new LDAP(); - $access = new Access($con, $lw, $um, $helper, $config, $this->ncUserManager, $this->logger); - - if (!function_exists('ldap_explode_dn')) { - $this->markTestSkipped('LDAP Module not available'); - } - - $this->assertSame($case['expectedResult'], $access->stringResemblesDN($case['input'])); - } - - public function testCacheUserHome() { - $this->connection->expects($this->once()) - ->method('writeToCache'); - - $this->access->cacheUserHome('foobar', '/foobars/path'); - } - - public function testBatchApplyUserAttributes() { - $this->ldap->expects($this->any()) - ->method('isResource') - ->willReturn(true); - - $this->ldap->expects($this->any()) - ->method('getAttributes') - ->willReturn(['displayname' => ['bar', 'count' => 1]]); - - /** @var UserMapping|\PHPUnit\Framework\MockObject\MockObject $mapperMock */ - $mapperMock = $this->createMock(UserMapping::class); - $mapperMock->expects($this->any()) - ->method('getNameByDN') - ->willReturn(false); - $mapperMock->expects($this->any()) - ->method('map') - ->willReturn(true); - - $userMock = $this->createMock(User::class); - - // also returns for userUuidAttribute - $this->access->connection->expects($this->any()) - ->method('__get') - ->willReturn('displayName'); - - $this->access->setUserMapper($mapperMock); - - $displayNameAttribute = strtolower($this->access->connection->ldapUserDisplayName); - $data = [ - [ - 'dn' => ['foobar'], - $displayNameAttribute => 'barfoo' - ], - [ - 'dn' => ['foo'], - $displayNameAttribute => 'bar' - ], - [ - 'dn' => ['raboof'], - $displayNameAttribute => 'oofrab' - ] - ]; - - $userMock->expects($this->exactly(count($data))) - ->method('processAttributes'); - - $this->userManager->expects($this->exactly(count($data) * 2)) - ->method('get') - ->willReturn($userMock); - - $this->access->batchApplyUserAttributes($data); - } - - public function testBatchApplyUserAttributesSkipped() { - /** @var UserMapping|\PHPUnit\Framework\MockObject\MockObject $mapperMock */ - $mapperMock = $this->createMock(UserMapping::class); - $mapperMock->expects($this->any()) - ->method('getNameByDN') - ->willReturn('a_username'); - - $userMock = $this->createMock(User::class); - - $this->access->connection->expects($this->any()) - ->method('__get') - ->willReturn('displayName'); - - $this->access->setUserMapper($mapperMock); - - $displayNameAttribute = strtolower($this->access->connection->ldapUserDisplayName); - $data = [ - [ - 'dn' => ['foobar'], - $displayNameAttribute => 'barfoo' - ], - [ - 'dn' => ['foo'], - $displayNameAttribute => 'bar' - ], - [ - 'dn' => ['raboof'], - $displayNameAttribute => 'oofrab' - ] - ]; - - $userMock->expects($this->never()) - ->method('processAttributes'); - - $this->userManager->expects($this->any()) - ->method('get') - ->willReturn($this->createMock(User::class)); - - $this->access->batchApplyUserAttributes($data); - } - - public function testBatchApplyUserAttributesDontSkip() { - /** @var UserMapping|\PHPUnit\Framework\MockObject\MockObject $mapperMock */ - $mapperMock = $this->createMock(UserMapping::class); - $mapperMock->expects($this->any()) - ->method('getNameByDN') - ->willReturn('a_username'); - - $userMock = $this->createMock(User::class); - - $this->access->connection->expects($this->any()) - ->method('__get') - ->willReturn('displayName'); - - $this->access->setUserMapper($mapperMock); - - $displayNameAttribute = strtolower($this->access->connection->ldapUserDisplayName); - $data = [ - [ - 'dn' => ['foobar'], - $displayNameAttribute => 'barfoo' - ], - [ - 'dn' => ['foo'], - $displayNameAttribute => 'bar' - ], - [ - 'dn' => ['raboof'], - $displayNameAttribute => 'oofrab' - ] - ]; - - $userMock->expects($this->exactly(count($data))) - ->method('processAttributes'); - - $this->userManager->expects($this->exactly(count($data) * 2)) - ->method('get') - ->willReturn($userMock); - - $this->access->batchApplyUserAttributes($data); - } - - public function dNAttributeProvider() { - // corresponds to Access::resemblesDN() - return [ - 'dn' => ['dn'], - 'uniqueMember' => ['uniquemember'], - 'member' => ['member'], - 'memberOf' => ['memberof'] - ]; - } - - /** - * @dataProvider dNAttributeProvider - * @param $attribute - */ - public function testSanitizeDN($attribute) { - list($lw, $con, $um, $helper) = $this->getConnectorAndLdapMock(); - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject $config */ - $config = $this->createMock(IConfig::class); - - $dnFromServer = 'cn=Mixed Cases,ou=Are Sufficient To,ou=Test,dc=example,dc=org'; - - $lw->expects($this->any()) - ->method('isResource') - ->willReturn(true); - $lw->expects($this->any()) - ->method('getAttributes') - ->willReturn([ - $attribute => ['count' => 1, $dnFromServer] - ]); - - $access = new Access($con, $lw, $um, $helper, $config, $this->ncUserManager, $this->logger); - $values = $access->readAttribute('uid=whoever,dc=example,dc=org', $attribute); - $this->assertSame($values[0], strtolower($dnFromServer)); - } - - - public function testSetPasswordWithDisabledChanges() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('LDAP password changes are disabled'); - - $this->connection - ->method('__get') - ->willReturn(false); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->access->setPassword('CN=foo', 'MyPassword'); - } - - public function testSetPasswordWithLdapNotAvailable() { - $this->connection - ->method('__get') - ->willReturn(true); - $connection = $this->createMock(LDAP::class); - $this->connection - ->expects($this->once()) - ->method('getConnectionResource') - ->willReturn($connection); - $this->ldap - ->expects($this->once()) - ->method('isResource') - ->with($connection) - ->willReturn(false); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->assertFalse($this->access->setPassword('CN=foo', 'MyPassword')); - } - - - public function testSetPasswordWithRejectedChange() { - $this->expectException(\OC\HintException::class); - $this->expectExceptionMessage('Password change rejected.'); - - $this->connection - ->method('__get') - ->willReturn(true); - $connection = $this->createMock(LDAP::class); - $this->connection - ->expects($this->once()) - ->method('getConnectionResource') - ->willReturn($connection); - $this->ldap - ->expects($this->once()) - ->method('isResource') - ->with($connection) - ->willReturn(true); - $this->ldap - ->expects($this->once()) - ->method('modReplace') - ->with($connection, 'CN=foo', 'MyPassword') - ->willThrowException(new ConstraintViolationException()); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->access->setPassword('CN=foo', 'MyPassword'); - } - - public function testSetPassword() { - $this->connection - ->method('__get') - ->willReturn(true); - $connection = $this->createMock(LDAP::class); - $this->connection - ->expects($this->once()) - ->method('getConnectionResource') - ->willReturn($connection); - $this->ldap - ->expects($this->once()) - ->method('isResource') - ->with($connection) - ->willReturn(true); - $this->ldap - ->expects($this->once()) - ->method('modReplace') - ->with($connection, 'CN=foo', 'MyPassword') - ->willReturn(true); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->assertTrue($this->access->setPassword('CN=foo', 'MyPassword')); - } - - protected function prepareMocksForSearchTests( - $base, - $fakeConnection, - $fakeSearchResultResource, - $fakeLdapEntries - ) { - $this->connection - ->expects($this->any()) - ->method('getConnectionResource') - ->willReturn($fakeConnection); - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($key) use ($base) { - if (stripos($key, 'base') !== false) { - return [$base]; - } - return null; - }); - - $this->ldap - ->expects($this->any()) - ->method('isResource') - ->willReturnCallback(function ($resource) { - return is_resource($resource); - }); - $this->ldap - ->expects($this->any()) - ->method('errno') - ->willReturn(0); - $this->ldap - ->expects($this->once()) - ->method('search') - ->willReturn($fakeSearchResultResource); - $this->ldap - ->expects($this->exactly(1)) - ->method('getEntries') - ->willReturn($fakeLdapEntries); - - $this->helper->expects($this->any()) - ->method('sanitizeDN') - ->willReturnArgument(0); - } - - public function testSearchNoPagedSearch() { - // scenario: no pages search, 1 search base - $filter = 'objectClass=nextcloudUser'; - $base = 'ou=zombies,dc=foobar,dc=nextcloud,dc=com'; - - $fakeConnection = ldap_connect(); - $fakeSearchResultResource = ldap_connect(); - $fakeLdapEntries = [ - 'count' => 2, - [ - 'dn' => 'uid=sgarth,' . $base, - ], - [ - 'dn' => 'uid=wwilson,' . $base, - ] - ]; - - $expected = $fakeLdapEntries; - unset($expected['count']); - - $this->prepareMocksForSearchTests($base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries); - - /** @noinspection PhpUnhandledExceptionInspection */ - $result = $this->access->search($filter, $base); - $this->assertSame($expected, $result); - } - - public function testFetchListOfUsers() { - $filter = 'objectClass=nextcloudUser'; - $base = 'ou=zombies,dc=foobar,dc=nextcloud,dc=com'; - $attrs = ['dn', 'uid']; - - $fakeConnection = ldap_connect(); - $fakeSearchResultResource = ldap_connect(); - $fakeLdapEntries = [ - 'count' => 2, - [ - 'dn' => 'uid=sgarth,' . $base, - 'uid' => [ 'sgarth' ], - ], - [ - 'dn' => 'uid=wwilson,' . $base, - 'uid' => [ 'wwilson' ], - ] - ]; - $expected = $fakeLdapEntries; - unset($expected['count']); - array_walk($expected, function (&$v) { - $v['dn'] = [$v['dn']]; // dn is translated into an array internally for consistency - }); - - $this->prepareMocksForSearchTests($base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries); - - $this->connection->expects($this->exactly($fakeLdapEntries['count'])) - ->method('writeToCache') - ->with($this->stringStartsWith('userExists'), true); - - $this->userMapper->expects($this->exactly($fakeLdapEntries['count'])) - ->method('getNameByDN') - ->willReturnCallback(function ($fdn) { - $parts = ldap_explode_dn($fdn, false); - return $parts[0]; - }); - - /** @noinspection PhpUnhandledExceptionInspection */ - $list = $this->access->fetchListOfUsers($filter, $attrs); - $this->assertSame($expected, $list); - } - - public function testFetchListOfGroupsKnown() { - $filter = 'objectClass=nextcloudGroup'; - $attributes = ['cn', 'gidNumber', 'dn']; - $base = 'ou=SomeGroups,dc=my,dc=directory'; - - $fakeConnection = ldap_connect(); - $fakeSearchResultResource = ldap_connect(); - $fakeLdapEntries = [ - 'count' => 2, - [ - 'dn' => 'cn=Good Team,' . $base, - 'cn' => ['Good Team'], - ], - [ - 'dn' => 'cn=Another Good Team,' . $base, - 'cn' => ['Another Good Team'], - ] - ]; - - $this->prepareMocksForSearchTests($base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries); - - $this->groupMapper->expects($this->any()) - ->method('getListOfIdsByDn') - ->willReturn([ - 'cn=Good Team,' . $base => 'Good_Team', - 'cn=Another Good Team,' . $base => 'Another_Good_Team', - ]); - $this->groupMapper->expects($this->never()) - ->method('getNameByDN'); - - $this->connection->expects($this->exactly(2)) - ->method('writeToCache'); - - $groups = $this->access->fetchListOfGroups($filter, $attributes); - $this->assertSame(2, count($groups)); - $this->assertSame('Good Team', $groups[0]['cn'][0]); - $this->assertSame('Another Good Team', $groups[1]['cn'][0]); - } - - public function intUsernameProvider() { - // system dependent :-/ - $translitExpected = @iconv('UTF-8', 'ASCII//TRANSLIT', 'fränk') ? 'frank' : 'frnk'; - - return [ - ['alice', 'alice'], - ['b/ob', 'bob'], - ['charly🐬', 'charly'], - ['debo rah', 'debo_rah'], - ['epost@poste.test', 'epost@poste.test'], - ['fränk', $translitExpected], - [' gerda ', 'gerda'], - ['🕱🐵🐘🐑', null], - [ - 'OneNameToRuleThemAllOneNameToFindThemOneNameToBringThemAllAndInTheDarknessBindThem', - '81ff71b5dd0f0092e2dc977b194089120093746e273f2ef88c11003762783127' - ] - ]; - } - - public function groupIDCandidateProvider() { - return [ - ['alice', 'alice'], - ['b/ob', 'b/ob'], - ['charly🐬', 'charly🐬'], - ['debo rah', 'debo rah'], - ['epost@poste.test', 'epost@poste.test'], - ['fränk', 'fränk'], - [' gerda ', 'gerda'], - ['🕱🐵🐘🐑', '🕱🐵🐘🐑'], - [ - 'OneNameToRuleThemAllOneNameToFindThemOneNameToBringThemAllAndInTheDarknessBindThem', - '81ff71b5dd0f0092e2dc977b194089120093746e273f2ef88c11003762783127' - ] - ]; - } - - /** - * @dataProvider intUsernameProvider - * - * @param $name - * @param $expected - */ - public function testSanitizeUsername($name, $expected) { - if ($expected === null) { - $this->expectException(\InvalidArgumentException::class); - } - $sanitizedName = $this->access->sanitizeUsername($name); - $this->assertSame($expected, $sanitizedName); - } - - /** - * @dataProvider groupIDCandidateProvider - */ - public function testSanitizeGroupIDCandidate(string $name, string $expected) { - $sanitizedName = $this->access->sanitizeGroupIDCandidate($name); - $this->assertSame($expected, $sanitizedName); - } - - public function testUserStateUpdate() { - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnMap([ - [ 'ldapUserDisplayName', 'displayName' ], - [ 'ldapUserDisplayName2', null], - ]); - - $offlineUserMock = $this->createMock(OfflineUser::class); - $offlineUserMock->expects($this->once()) - ->method('unmark'); - - $regularUserMock = $this->createMock(User::class); - - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->with('detta') - ->willReturnOnConsecutiveCalls($offlineUserMock, $regularUserMock); - - /** @var UserMapping|\PHPUnit\Framework\MockObject\MockObject $mapperMock */ - $mapperMock = $this->createMock(UserMapping::class); - $mapperMock->expects($this->any()) - ->method('getNameByDN') - ->with('uid=detta,ou=users,dc=hex,dc=ample') - ->willReturn('detta'); - $this->access->setUserMapper($mapperMock); - - $records = [ - [ - 'dn' => ['uid=detta,ou=users,dc=hex,dc=ample'], - 'displayName' => ['Detta Detkova'], - ] - ]; - $this->access->nextcloudUserNames($records); - } -} diff --git a/apps/user_ldap/tests/ConfigurationTest.php b/apps/user_ldap/tests/ConfigurationTest.php deleted file mode 100644 index db119eb3bfe60..0000000000000 --- a/apps/user_ldap/tests/ConfigurationTest.php +++ /dev/null @@ -1,142 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Roeland Jago Douma - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\Configuration; - -class ConfigurationTest extends \Test\TestCase { - /** @var Configuration */ - protected $configuration; - - protected function setUp(): void { - parent::setUp(); - $this->configuration = new Configuration('t01', false); - } - - public function configurationDataProvider() { - $inputWithDN = [ - 'cn=someUsers,dc=example,dc=org', - ' ', - ' cn=moreUsers,dc=example,dc=org ' - ]; - $expectWithDN = [ - 'cn=someUsers,dc=example,dc=org', - 'cn=moreUsers,dc=example,dc=org' - ]; - - $inputNames = [ - ' uid ', - 'cn ', - ' ', - '', - ' whats my name', - ' ' - ]; - $expectedNames = ['uid', 'cn', 'whats my name']; - - $inputString = ' alea iacta est '; - $expectedString = 'alea iacta est'; - - $inputHomeFolder = [ - ' homeDirectory ', - ' attr:homeDirectory ', - ' ' - ]; - - $expectedHomeFolder = [ - 'attr:homeDirectory', 'attr:homeDirectory', '' - ]; - - $password = ' such a passw0rd '; - - return [ - 'set general base' => ['ldapBase', $inputWithDN, $expectWithDN], - 'set user base' => ['ldapBaseUsers', $inputWithDN, $expectWithDN], - 'set group base' => ['ldapBaseGroups', $inputWithDN, $expectWithDN], - - 'set search attributes users' => ['ldapAttributesForUserSearch', $inputNames, $expectedNames], - 'set search attributes groups' => ['ldapAttributesForGroupSearch', $inputNames, $expectedNames], - - 'set user filter objectclasses' => ['ldapUserFilterObjectclass', $inputNames, $expectedNames], - 'set user filter groups' => ['ldapUserFilterGroups', $inputNames, $expectedNames], - 'set group filter objectclasses' => ['ldapGroupFilterObjectclass', $inputNames, $expectedNames], - 'set group filter groups' => ['ldapGroupFilterGroups', $inputNames, $expectedNames], - 'set login filter attributes' => ['ldapLoginFilterAttributes', $inputNames, $expectedNames], - - 'set agent password' => ['ldapAgentPassword', $password, $password], - - 'set home folder, variant 1' => ['homeFolderNamingRule', $inputHomeFolder[0], $expectedHomeFolder[0]], - 'set home folder, variant 2' => ['homeFolderNamingRule', $inputHomeFolder[1], $expectedHomeFolder[1]], - 'set home folder, empty' => ['homeFolderNamingRule', $inputHomeFolder[2], $expectedHomeFolder[2]], - - // default behaviour, one case is enough, special needs must be tested - // individually - 'set string value' => ['ldapHost', $inputString, $expectedString], - - 'set avatar rule, default' => ['ldapUserAvatarRule', 'default', 'default'], - 'set avatar rule, none' => ['ldapUserAvatarRule', 'none', 'none'], - 'set avatar rule, data attribute' => ['ldapUserAvatarRule', 'data:jpegPhoto', 'data:jpegPhoto'], - - 'set external storage home attribute' => ['ldapExtStorageHomeAttribute', 'homePath', 'homePath'], - ]; - } - - /** - * @dataProvider configurationDataProvider - */ - public function testSetValue($key, $input, $expected) { - $this->configuration->setConfiguration([$key => $input]); - $this->assertSame($this->configuration->$key, $expected); - } - - public function avatarRuleValueProvider() { - return [ - ['none', []], - ['data:selfie', ['selfie']], - ['data:sELFie', ['selfie']], - ['data:', ['jpegphoto', 'thumbnailphoto']], - ['default', ['jpegphoto', 'thumbnailphoto']], - ['invalid#', ['jpegphoto', 'thumbnailphoto']], - ]; - } - - /** - * @dataProvider avatarRuleValueProvider - */ - public function testGetAvatarAttributes($setting, $expected) { - $this->configuration->setConfiguration(['ldapUserAvatarRule' => $setting]); - $this->assertSame($expected, $this->configuration->getAvatarAttributes()); - } - - /** - * @dataProvider avatarRuleValueProvider - */ - public function testResolveRule($setting, $expected) { - $this->configuration->setConfiguration(['ldapUserAvatarRule' => $setting]); - // so far the only thing that can get resolved :) - $this->assertSame($expected, $this->configuration->resolveRule('avatar')); - } -} diff --git a/apps/user_ldap/tests/ConnectionTest.php b/apps/user_ldap/tests/ConnectionTest.php deleted file mode 100644 index a4fcf03008378..0000000000000 --- a/apps/user_ldap/tests/ConnectionTest.php +++ /dev/null @@ -1,291 +0,0 @@ - - * @author Christoph Wurst - * @author Jarkko Lehtoranta - * @author Joas Schilling - * @author Julius Härtl - * @author Morris Jobke - * @author Roeland Jago Douma - * @author Thomas Müller - * @author Victor Dubiniuk - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\Connection; -use OCA\User_LDAP\ILDAPWrapper; - -/** - * Class Test_Connection - * - * @group DB - * - * @package OCA\User_LDAP\Tests - */ -class ConnectionTest extends \Test\TestCase { - /** @var \OCA\User_LDAP\ILDAPWrapper|\PHPUnit\Framework\MockObject\MockObject */ - protected $ldap; - - /** @var Connection */ - protected $connection; - - protected function setUp(): void { - parent::setUp(); - - $this->ldap = $this->createMock(ILDAPWrapper::class); - // we use a mock here to replace the cache mechanism, due to missing DI in LDAP backend. - $this->connection = $this->getMockBuilder('OCA\User_LDAP\Connection') - ->setMethods(['getFromCache', 'writeToCache']) - ->setConstructorArgs([$this->ldap, '', null]) - ->getMock(); - - $this->ldap->expects($this->any()) - ->method('areLDAPFunctionsAvailable') - ->willReturn(true); - } - - public function testOriginalAgentUnchangedOnClone() { - //background: upon login a bind is done with the user credentials - //which is valid for the whole LDAP resource. It needs to be reset - //to the agent's credentials - $lw = $this->createMock(ILDAPWrapper::class); - - $connection = new Connection($lw, '', null); - $agent = [ - 'ldapAgentName' => 'agent', - 'ldapAgentPassword' => '123456', - ]; - $connection->setConfiguration($agent); - - $testConnection = clone $connection; - $user = [ - 'ldapAgentName' => 'user', - 'ldapAgentPassword' => 'password', - ]; - $testConnection->setConfiguration($user); - - $agentName = $connection->ldapAgentName; - $agentPawd = $connection->ldapAgentPassword; - - $this->assertSame($agentName, $agent['ldapAgentName']); - $this->assertSame($agentPawd, $agent['ldapAgentPassword']); - } - - public function testUseBackupServer() { - $mainHost = 'ldap://nixda.ldap'; - $backupHost = 'ldap://fallback.ldap'; - $config = [ - 'ldapConfigurationActive' => true, - 'ldapHost' => $mainHost, - 'ldapPort' => 389, - 'ldapBackupHost' => $backupHost, - 'ldapBackupPort' => 389, - 'ldapAgentName' => 'uid=agent', - 'ldapAgentPassword' => 'SuchASecret' - ]; - - $this->connection->setIgnoreValidation(true); - $this->connection->setConfiguration($config); - - $this->ldap->expects($this->any()) - ->method('isResource') - ->willReturn(true); - - $this->ldap->expects($this->any()) - ->method('setOption') - ->willReturn(true); - - $this->ldap->expects($this->exactly(3)) - ->method('connect') - ->willReturn('ldapResource'); - - $this->ldap->expects($this->any()) - ->method('errno') - ->willReturn(0); - - // Not called often enough? Then, the fallback to the backup server is broken. - $this->connection->expects($this->exactly(4)) - ->method('getFromCache') - ->with('overrideMainServer') - ->will($this->onConsecutiveCalls(false, false, true, true)); - - $this->connection->expects($this->once()) - ->method('writeToCache') - ->with('overrideMainServer', true); - - $isThrown = false; - $this->ldap->expects($this->exactly(3)) - ->method('bind') - ->willReturnCallback(function () use (&$isThrown) { - if (!$isThrown) { - $isThrown = true; - throw new \OC\ServerNotAvailableException(); - } - return true; - }); - - $this->connection->init(); - $this->connection->resetConnectionResource(); - // with the second init() we test whether caching works - $this->connection->init(); - } - - public function testDontUseBackupServerOnFailedAuth() { - $mainHost = 'ldap://nixda.ldap'; - $backupHost = 'ldap://fallback.ldap'; - $config = [ - 'ldapConfigurationActive' => true, - 'ldapHost' => $mainHost, - 'ldapPort' => 389, - 'ldapBackupHost' => $backupHost, - 'ldapBackupPort' => 389, - 'ldapAgentName' => 'uid=agent', - 'ldapAgentPassword' => 'SuchASecret' - ]; - - $this->connection->setIgnoreValidation(true); - $this->connection->setConfiguration($config); - - $this->ldap->expects($this->any()) - ->method('isResource') - ->willReturn(true); - - $this->ldap->expects($this->any()) - ->method('setOption') - ->willReturn(true); - - $this->ldap->expects($this->once()) - ->method('connect') - ->willReturn('ldapResource'); - - $this->ldap->expects($this->any()) - ->method('errno') - ->willReturn(49); - - $this->connection->expects($this->any()) - ->method('getFromCache') - ->with('overrideMainServer') - ->willReturn(false); - - $this->connection->expects($this->never()) - ->method('writeToCache'); - - $this->ldap->expects($this->exactly(1)) - ->method('bind') - ->willReturn(false); - - $this->connection->init(); - } - - public function testBindWithInvalidCredentials() { - // background: Bind with invalid credentials should return false - // and not throw a ServerNotAvailableException. - - $host = 'ldap://nixda.ldap'; - $config = [ - 'ldapConfigurationActive' => true, - 'ldapHost' => $host, - 'ldapPort' => 389, - 'ldapBackupHost' => '', - 'ldapAgentName' => 'user', - 'ldapAgentPassword' => 'password' - ]; - - $this->connection->setIgnoreValidation(true); - $this->connection->setConfiguration($config); - - $this->ldap->expects($this->any()) - ->method('isResource') - ->willReturn(true); - - $this->ldap->expects($this->any()) - ->method('setOption') - ->willReturn(true); - - $this->ldap->expects($this->any()) - ->method('connect') - ->willReturn('ldapResource'); - - $this->ldap->expects($this->once()) - ->method('bind') - ->willReturn(false); - - // LDAP_INVALID_CREDENTIALS - $this->ldap->expects($this->any()) - ->method('errno') - ->willReturn(0x31); - - try { - $this->assertFalse($this->connection->bind(), 'Connection::bind() should not return true with invalid credentials.'); - } catch (\OC\ServerNotAvailableException $e) { - $this->fail('Failed asserting that exception of type "OC\ServerNotAvailableException" is not thrown.'); - } - } - - public function testStartTlsNegotiationFailure() { - // background: If Start TLS negotiation fails, - // a ServerNotAvailableException should be thrown. - - $host = 'ldap://nixda.ldap'; - $port = 389; - $config = [ - 'ldapConfigurationActive' => true, - 'ldapHost' => $host, - 'ldapPort' => $port, - 'ldapTLS' => true, - 'ldapBackupHost' => '', - 'ldapAgentName' => 'user', - 'ldapAgentPassword' => 'password' - ]; - - $this->connection->setIgnoreValidation(true); - $this->connection->setConfiguration($config); - - $this->ldap->expects($this->any()) - ->method('isResource') - ->willReturn(true); - - $this->ldap->expects($this->any()) - ->method('connect') - ->willReturn('ldapResource'); - - $this->ldap->expects($this->any()) - ->method('setOption') - ->willReturn(true); - - $this->ldap->expects($this->any()) - ->method('bind') - ->willReturn(true); - - $this->ldap->expects($this->any()) - ->method('errno') - ->willReturn(0); - - $this->ldap->expects($this->any()) - ->method('startTls') - ->willReturn(false); - - $this->expectException(\OC\ServerNotAvailableException::class); - $this->expectExceptionMessage('Start TLS failed, when connecting to LDAP host ' . $host . '.'); - - $this->connection->init(); - } -} diff --git a/apps/user_ldap/tests/GroupLDAPPluginTest.php b/apps/user_ldap/tests/GroupLDAPPluginTest.php deleted file mode 100644 index baabdfb53c46d..0000000000000 --- a/apps/user_ldap/tests/GroupLDAPPluginTest.php +++ /dev/null @@ -1,249 +0,0 @@ - - * @author Roeland Jago Douma - * @author Vinicius Cubas Brand - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\GroupPluginManager; -use OCP\GroupInterface; - -class GroupLDAPPluginTest extends \Test\TestCase { - - /** - * @return GroupPluginManager - */ - private function getGroupPluginManager() { - return new GroupPluginManager(); - } - - public function testImplementsActions() { - $pluginManager = $this->getGroupPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPGroupPluginDummy') - ->setMethods(['respondToActions']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(GroupInterface::CREATE_GROUP); - - $plugin2 = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPGroupPluginDummy') - ->setMethods(['respondToActions']) - ->getMock(); - - $plugin2->expects($this->any()) - ->method('respondToActions') - ->willReturn(GroupInterface::ADD_TO_GROUP); - - $pluginManager->register($plugin); - $pluginManager->register($plugin2); - - $this->assertEquals($pluginManager->getImplementedActions(), GroupInterface::CREATE_GROUP | GroupInterface::ADD_TO_GROUP); - $this->assertTrue($pluginManager->implementsActions(GroupInterface::CREATE_GROUP)); - $this->assertTrue($pluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)); - } - - public function testCreateGroup() { - $pluginManager = $this->getGroupPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPGroupPluginDummy') - ->setMethods(['respondToActions', 'createGroup']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(GroupInterface::CREATE_GROUP); - - $plugin->expects($this->once()) - ->method('createGroup') - ->with( - $this->equalTo('group') - ); - - $pluginManager->register($plugin); - $pluginManager->createGroup('group'); - } - - - public function testCreateGroupNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements createGroup in this LDAP Backend.'); - - $pluginManager = $this->getGroupPluginManager(); - $pluginManager->createGroup('foo'); - } - - public function testDeleteGroup() { - $pluginManager = $this->getGroupPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPGroupPluginDummy') - ->setMethods(['respondToActions', 'deleteGroup']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(GroupInterface::DELETE_GROUP); - - $plugin->expects($this->once()) - ->method('deleteGroup') - ->with( - $this->equalTo('group') - ); - - $pluginManager->register($plugin); - $pluginManager->deleteGroup('group'); - } - - - public function testDeleteGroupNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements deleteGroup in this LDAP Backend.'); - - $pluginManager = $this->getGroupPluginManager(); - $pluginManager->deleteGroup('foo'); - } - - public function testAddToGroup() { - $pluginManager = $this->getGroupPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPGroupPluginDummy') - ->setMethods(['respondToActions', 'addToGroup']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(GroupInterface::ADD_TO_GROUP); - - $plugin->expects($this->once()) - ->method('addToGroup') - ->with( - $this->equalTo('uid'), - $this->equalTo('gid') - ); - - $pluginManager->register($plugin); - $pluginManager->addToGroup('uid', 'gid'); - } - - - public function testAddToGroupNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements addToGroup in this LDAP Backend.'); - - $pluginManager = $this->getGroupPluginManager(); - $pluginManager->addToGroup('foo', 'bar'); - } - - public function testRemoveFromGroup() { - $pluginManager = $this->getGroupPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPGroupPluginDummy') - ->setMethods(['respondToActions', 'removeFromGroup']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(GroupInterface::REMOVE_FROM_GROUP); - - $plugin->expects($this->once()) - ->method('removeFromGroup') - ->with( - $this->equalTo('uid'), - $this->equalTo('gid') - ); - - $pluginManager->register($plugin); - $pluginManager->removeFromGroup('uid', 'gid'); - } - - - public function testRemoveFromGroupNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements removeFromGroup in this LDAP Backend.'); - - $pluginManager = $this->getGroupPluginManager(); - $pluginManager->removeFromGroup('foo', 'bar'); - } - - public function testCountUsersInGroup() { - $pluginManager = $this->getGroupPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPGroupPluginDummy') - ->setMethods(['respondToActions', 'countUsersInGroup']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(GroupInterface::COUNT_USERS); - - $plugin->expects($this->once()) - ->method('countUsersInGroup') - ->with( - $this->equalTo('gid'), - $this->equalTo('search') - ); - - $pluginManager->register($plugin); - $pluginManager->countUsersInGroup('gid', 'search'); - } - - - public function testCountUsersInGroupNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements countUsersInGroup in this LDAP Backend.'); - - $pluginManager = $this->getGroupPluginManager(); - $pluginManager->countUsersInGroup('foo', 'bar'); - } - - public function testgetGroupDetails() { - $pluginManager = $this->getGroupPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPGroupPluginDummy') - ->setMethods(['respondToActions', 'getGroupDetails']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(GroupInterface::GROUP_DETAILS); - - $plugin->expects($this->once()) - ->method('getGroupDetails') - ->with( - $this->equalTo('gid') - ); - - $pluginManager->register($plugin); - $pluginManager->getGroupDetails('gid'); - } - - - public function testgetGroupDetailsNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements getGroupDetails in this LDAP Backend.'); - - $pluginManager = $this->getGroupPluginManager(); - $pluginManager->getGroupDetails('foo'); - } -} diff --git a/apps/user_ldap/tests/Group_LDAPTest.php b/apps/user_ldap/tests/Group_LDAPTest.php deleted file mode 100644 index 1844e2deddcbc..0000000000000 --- a/apps/user_ldap/tests/Group_LDAPTest.php +++ /dev/null @@ -1,1383 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Morris Jobke - * @author Roeland Jago Douma - * @author Thomas Müller - * @author Victor Dubiniuk - * @author Vincent Petry - * @author Vinicius Cubas Brand - * @author Xuanwo - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\Access; -use OCA\User_LDAP\Connection; -use OCA\User_LDAP\Group_LDAP as GroupLDAP; -use OCA\User_LDAP\GroupPluginManager; -use OCA\User_LDAP\ILDAPWrapper; -use OCA\User_LDAP\Mapping\GroupMapping; -use OCA\User_LDAP\User\Manager; -use OCP\GroupInterface; -use PHPUnit\Framework\MockObject\MockObject; -use Test\TestCase; - -/** - * Class GroupLDAPTest - * - * @group DB - * - * @package OCA\User_LDAP\Tests - */ -class Group_LDAPTest extends TestCase { - public function testCountEmptySearchString() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - $groupDN = 'cn=group,dc=foo,dc=bar'; - - $this->enableGroups($access); - - $access->expects($this->any()) - ->method('groupname2dn') - ->willReturn($groupDN); - $access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn) use ($groupDN) { - if ($dn === $groupDN) { - return [ - 'uid=u11,ou=users,dc=foo,dc=bar', - 'uid=u22,ou=users,dc=foo,dc=bar', - 'uid=u33,ou=users,dc=foo,dc=bar', - 'uid=u34,ou=users,dc=foo,dc=bar' - ]; - } - return []; - }); - $access->expects($this->any()) - ->method('isDNPartOfBase') - ->willReturn(true); - // for primary groups - $access->expects($this->once()) - ->method('countUsers') - ->willReturn(2); - - $access->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['displayName', 'mail']); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $users = $groupBackend->countUsersInGroup('group'); - - $this->assertSame(6, $users); - } - - /** - * @return MockObject|Access - */ - private function getAccessMock() { - static $conMethods; - static $accMethods; - - if (is_null($conMethods) || is_null($accMethods)) { - $conMethods = get_class_methods(Connection::class); - $accMethods = get_class_methods(Access::class); - } - $lw = $this->createMock(ILDAPWrapper::class); - $connector = $this->getMockBuilder(Connection::class) - ->setMethods($conMethods) - ->setConstructorArgs([$lw, null, null]) - ->getMock(); - - $access = $this->createMock(Access::class); - - $access->expects($this->any()) - ->method('getConnection') - ->willReturn($connector); - - $access->userManager = $this->createMock(Manager::class); - - return $access; - } - - /** - * @return MockObject|GroupPluginManager - */ - private function getPluginManagerMock() { - return $this->createMock(GroupPluginManager::class); - } - - private function enableGroups(Access $access) { - $access->connection = $this->createMock(Connection::class); - - $access->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapDynamicGroupMemberURL') { - return ''; - } - return 1; - }); - } - - public function testCountWithSearchString() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $access->expects($this->any()) - ->method('groupname2dn') - ->willReturn('cn=group,dc=foo,dc=bar'); - $access->expects($this->any()) - ->method('fetchListOfUsers') - ->willReturn([]); - $access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($name) { - //the search operation will call readAttribute, thus we need - //to anaylze the "dn". All other times we just need to return - //something that is neither null or false, but once an array - //with the users in the group – so we do so all other times for - //simplicicity. - if (strpos($name, 'u') === 0) { - return strpos($name, '3'); - } - return ['u11', 'u22', 'u33', 'u34']; - }); - $access->expects($this->any()) - ->method('dn2username') - ->willReturnCallback(function () { - return 'foobar' . \OC::$server->getSecureRandom()->generate(7); - }); - $access->expects($this->any()) - ->method('isDNPartOfBase') - ->willReturn(true); - $access->expects($this->any()) - ->method('escapeFilterPart') - ->willReturnArgument(0); - - $access->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['displayName', 'mail']); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $users = $groupBackend->countUsersInGroup('group', '3'); - - $this->assertSame(2, $users); - } - - public function testCountUsersWithPlugin() { - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'countUsersInGroup']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::COUNT_USERS) - ->willReturn(true); - - $pluginManager->expects($this->once()) - ->method('countUsersInGroup') - ->with('gid', 'search') - ->willReturn(42); - - $access = $this->getAccessMock(); - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $this->assertEquals($ldap->countUsersInGroup('gid', 'search'), 42); - } - - public function testGidNumber2NameSuccess() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - - $access->expects($this->once()) - ->method('searchGroups') - ->willReturn([['dn' => ['cn=foo,dc=barfoo,dc=bar']]]); - - $access->expects($this->once()) - ->method('dn2groupname') - ->with('cn=foo,dc=barfoo,dc=bar') - ->willReturn('MyGroup'); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $group = $groupBackend->gidNumber2Name('3117', $userDN); - - $this->assertSame('MyGroup', $group); - } - - public function testGidNumberID2NameNoGroup() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - - $access->expects($this->once()) - ->method('searchGroups') - ->willReturn([]); - - $access->expects($this->never()) - ->method('dn2groupname'); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $group = $groupBackend->gidNumber2Name('3117', $userDN); - - $this->assertSame(false, $group); - } - - public function testGidNumberID2NameNoName() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - - $access->expects($this->once()) - ->method('searchGroups') - ->willReturn([['dn' => ['cn=foo,dc=barfoo,dc=bar']]]); - - $access->expects($this->once()) - ->method('dn2groupname') - ->willReturn(false); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $group = $groupBackend->gidNumber2Name('3117', $userDN); - - $this->assertSame(false, $group); - } - - public function testGetEntryGidNumberValue() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $dn = 'cn=foobar,cn=foo,dc=barfoo,dc=bar'; - $attr = 'gidNumber'; - - $access->expects($this->once()) - ->method('readAttribute') - ->with($dn, $attr) - ->willReturn(['3117']); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $gid = $groupBackend->getGroupGidNumber($dn); - - $this->assertSame('3117', $gid); - } - - public function testGetEntryGidNumberNoValue() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $dn = 'cn=foobar,cn=foo,dc=barfoo,dc=bar'; - $attr = 'gidNumber'; - - $access->expects($this->once()) - ->method('readAttribute') - ->with($dn, $attr) - ->willReturn(false); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $gid = $groupBackend->getGroupGidNumber($dn); - - $this->assertSame(false, $gid); - } - - public function testPrimaryGroupID2NameSuccessCache() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - $gid = '3117'; - $groupDN = 'cn=foo,dc=barfoo,dc=bar'; - - /** @var MockObject $connection */ - $connection = $access->connection; - $connection->expects($this->once()) - ->method('getFromCache') - ->with('primaryGroupIDtoName_' . $gid) - ->willReturn('MyGroup'); - - $access->expects($this->never()) - ->method('getSID'); - - $access->expects($this->never()) - ->method('searchGroups'); - - $access->expects($this->never()) - ->method('dn2groupname'); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $group = $groupBackend->primaryGroupID2Name($gid, $userDN); - - $this->assertSame('MyGroup', $group); - } - - public function testPrimaryGroupID2NameSuccess() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - - $access->expects($this->once()) - ->method('getSID') - ->with($userDN) - ->willReturn('S-1-5-21-249921958-728525901-1594176202'); - - $access->expects($this->once()) - ->method('searchGroups') - ->willReturn([['dn' => ['cn=foo,dc=barfoo,dc=bar']]]); - - $access->expects($this->once()) - ->method('dn2groupname') - ->with('cn=foo,dc=barfoo,dc=bar') - ->willReturn('MyGroup'); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $group = $groupBackend->primaryGroupID2Name('3117', $userDN); - - $this->assertSame('MyGroup', $group); - } - - public function testPrimaryGroupID2NameNoSID() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - - $access->expects($this->once()) - ->method('getSID') - ->with($userDN) - ->willReturn(false); - - $access->expects($this->never()) - ->method('searchGroups'); - - $access->expects($this->never()) - ->method('dn2groupname'); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $group = $groupBackend->primaryGroupID2Name('3117', $userDN); - - $this->assertSame(false, $group); - } - - public function testPrimaryGroupID2NameNoGroup() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - - $access->expects($this->once()) - ->method('getSID') - ->with($userDN) - ->willReturn('S-1-5-21-249921958-728525901-1594176202'); - - $access->expects($this->once()) - ->method('searchGroups') - ->willReturn([]); - - $access->expects($this->never()) - ->method('dn2groupname'); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $group = $groupBackend->primaryGroupID2Name('3117', $userDN); - - $this->assertSame(false, $group); - } - - public function testPrimaryGroupID2NameNoName() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - - $access->expects($this->once()) - ->method('getSID') - ->with($userDN) - ->willReturn('S-1-5-21-249921958-728525901-1594176202'); - - $access->expects($this->once()) - ->method('searchGroups') - ->willReturn([['dn' => ['cn=foo,dc=barfoo,dc=bar']]]); - - $access->expects($this->once()) - ->method('dn2groupname') - ->willReturn(false); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $group = $groupBackend->primaryGroupID2Name('3117', $userDN); - - $this->assertSame(false, $group); - } - - public function testGetEntryGroupIDValue() { - //tests getEntryGroupID via getGroupPrimaryGroupID - //which is basically identical to getUserPrimaryGroupIDs - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $dn = 'cn=foobar,cn=foo,dc=barfoo,dc=bar'; - $attr = 'primaryGroupToken'; - - $access->expects($this->once()) - ->method('readAttribute') - ->with($dn, $attr) - ->willReturn(['3117']); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $gid = $groupBackend->getGroupPrimaryGroupID($dn); - - $this->assertSame('3117', $gid); - } - - public function testGetEntryGroupIDNoValue() { - //tests getEntryGroupID via getGroupPrimaryGroupID - //which is basically identical to getUserPrimaryGroupIDs - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $dn = 'cn=foobar,cn=foo,dc=barfoo,dc=bar'; - $attr = 'primaryGroupToken'; - - $access->expects($this->once()) - ->method('readAttribute') - ->with($dn, $attr) - ->willReturn(false); - - $groupBackend = new GroupLDAP($access, $pluginManager); - - $gid = $groupBackend->getGroupPrimaryGroupID($dn); - - $this->assertSame(false, $gid); - } - - /** - * tests whether Group Backend behaves correctly when cache with uid and gid - * is hit - */ - public function testInGroupHitsUidGidCache() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $uid = 'someUser'; - $gid = 'someGroup'; - $cacheKey = 'inGroup' . $uid . ':' . $gid; - - $access->connection->expects($this->once()) - ->method('getFromCache') - ->with($cacheKey) - ->willReturn(true); - - $access->expects($this->never()) - ->method('username2dn'); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $groupBackend->inGroup($uid, $gid); - } - - public function groupWithMembersProvider() { - return [ - [ - 'someGroup', - 'cn=someGroup,ou=allTheGroups,ou=someDepartment,dc=someDomain,dc=someTld', - [ - 'uid=oneUser,ou=someTeam,ou=someDepartment,dc=someDomain,dc=someTld', - 'uid=someUser,ou=someTeam,ou=someDepartment,dc=someDomain,dc=someTld', - 'uid=anotherUser,ou=someTeam,ou=someDepartment,dc=someDomain,dc=someTld', - 'uid=differentUser,ou=someTeam,ou=someDepartment,dc=someDomain,dc=someTld', - ], - ], - ]; - } - - /** - * @dataProvider groupWithMembersProvider - */ - public function testInGroupMember(string $gid, string $groupDn, array $memberDNs) { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $access->connection = $this->createMock(Connection::class); - - $uid = 'someUser'; - $userDn = $memberDNs[0]; - - $access->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - switch ($name) { - case 'ldapGroupMemberAssocAttr': - return 'member'; - case 'ldapDynamicGroupMemberURL': - return ''; - case 'hasPrimaryGroups': - case 'ldapNestedGroups': - return 0; - default: - return 1; - } - }); - $access->connection->expects($this->any()) - ->method('getFromCache') - ->willReturn(null); - - $access->expects($this->once()) - ->method('username2dn') - ->with($uid) - ->willReturn($userDn); - $access->expects($this->once()) - ->method('groupname2dn') - ->willReturn($groupDn); - $access->expects($this->any()) - ->method('readAttribute') - ->willReturn($memberDNs); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $this->assertTrue($groupBackend->inGroup($uid, $gid)); - } - - /** - * @dataProvider groupWithMembersProvider - */ - public function testInGroupMemberNot(string $gid, string $groupDn, array $memberDNs) { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $access->connection = $this->createMock(Connection::class); - - $uid = 'unelatedUser'; - $userDn = 'uid=unrelatedUser,ou=unrelatedTeam,ou=unrelatedDepartment,dc=someDomain,dc=someTld'; - - $access->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - switch ($name) { - case 'ldapGroupMemberAssocAttr': - return 'member'; - case 'ldapDynamicGroupMemberURL': - return ''; - case 'hasPrimaryGroups': - case 'ldapNestedGroups': - return 0; - default: - return 1; - } - }); - $access->connection->expects($this->any()) - ->method('getFromCache') - ->willReturn(null); - - $access->expects($this->once()) - ->method('username2dn') - ->with($uid) - ->willReturn($userDn); - $access->expects($this->once()) - ->method('groupname2dn') - ->willReturn($groupDn); - $access->expects($this->any()) - ->method('readAttribute') - ->willReturn($memberDNs); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $this->assertFalse($groupBackend->inGroup($uid, $gid)); - } - - /** - * @dataProvider groupWithMembersProvider - */ - public function testInGroupMemberUid(string $gid, string $groupDn, array $memberDNs) { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $memberUids = []; - $userRecords = []; - foreach ($memberDNs as $dn) { - $memberUids[] = ldap_explode_dn($dn, false)[0]; - $userRecords[] = ['dn' => [$dn]]; - } - - - $access->connection = $this->createMock(Connection::class); - - $uid = 'someUser'; - $userDn = $memberDNs[0]; - - $access->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - switch ($name) { - case 'ldapGroupMemberAssocAttr': - return 'memberUid'; - case 'ldapDynamicGroupMemberURL': - return ''; - case 'ldapLoginFilter': - return 'uid=%uid'; - case 'hasPrimaryGroups': - case 'ldapNestedGroups': - return 0; - default: - return 1; - } - }); - $access->connection->expects($this->any()) - ->method('getFromCache') - ->willReturn(null); - - $access->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['uid', 'mail', 'displayname']); - - $access->expects($this->once()) - ->method('username2dn') - ->with($uid) - ->willReturn($userDn); - $access->expects($this->once()) - ->method('groupname2dn') - ->willReturn($groupDn); - $access->expects($this->any()) - ->method('readAttribute') - ->willReturn($memberUids); - $access->expects($this->any()) - ->method('fetchListOfUsers') - ->willReturn($userRecords); - $access->expects($this->any()) - ->method('combineFilterWithOr') - ->willReturn('(|(pseudo=filter)(filter=pseudo))'); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $this->assertTrue($groupBackend->inGroup($uid, $gid)); - } - - public function testGetGroupsWithOffset() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $access->expects($this->once()) - ->method('nextcloudGroupNames') - ->willReturn(['group1', 'group2']); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $groups = $groupBackend->getGroups('', 2, 2); - - $this->assertSame(2, count($groups)); - } - - /** - * tests that a user listing is complete, if all it's members have the group - * as their primary. - */ - public function testUsersInGroupPrimaryMembersOnly() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $access->connection->expects($this->any()) - ->method('getFromCache') - ->willReturn(null); - $access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - if ($attr === 'primaryGroupToken') { - return [1337]; - } elseif ($attr === 'gidNumber') { - return [4211]; - } - return []; - }); - $access->expects($this->any()) - ->method('groupname2dn') - ->willReturn('cn=foobar,dc=foo,dc=bar'); - $access->expects($this->exactly(2)) - ->method('nextcloudUserNames') - ->willReturnOnConsecutiveCalls(['lisa', 'bart', 'kira', 'brad'], ['walle', 'dino', 'xenia']); - $access->expects($this->any()) - ->method('isDNPartOfBase') - ->willReturn(true); - $access->expects($this->any()) - ->method('combineFilterWithAnd') - ->willReturn('pseudo=filter'); - - $access->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['displayName', 'mail']); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $users = $groupBackend->usersInGroup('foobar'); - - $this->assertSame(7, count($users)); - } - - /** - * tests that a user listing is complete, if all it's members have the group - * as their primary. - */ - public function testUsersInGroupPrimaryAndUnixMembers() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $access->connection->expects($this->any()) - ->method('getFromCache') - ->willReturn(null); - $access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - if ($attr === 'primaryGroupToken') { - return [1337]; - } - return []; - }); - $access->expects($this->any()) - ->method('groupname2dn') - ->willReturn('cn=foobar,dc=foo,dc=bar'); - $access->expects($this->once()) - ->method('nextcloudUserNames') - ->willReturn(['lisa', 'bart', 'kira', 'brad']); - $access->expects($this->any()) - ->method('isDNPartOfBase') - ->willReturn(true); - $access->expects($this->any()) - ->method('combineFilterWithAnd') - ->willReturn('pseudo=filter'); - - $access->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['displayName', 'mail']); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $users = $groupBackend->usersInGroup('foobar'); - - $this->assertSame(4, count($users)); - } - - /** - * tests that a user counting is complete, if all it's members have the group - * as their primary. - */ - public function testCountUsersInGroupPrimaryMembersOnly() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $access->connection->expects($this->any()) - ->method('getFromCache') - ->willReturn(null); - - $access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - if ($attr === 'primaryGroupToken') { - return [1337]; - } - return []; - }); - $access->expects($this->any()) - ->method('groupname2dn') - ->willReturn('cn=foobar,dc=foo,dc=bar'); - $access->expects($this->once()) - ->method('countUsers') - ->willReturn(4); - $access->expects($this->any()) - ->method('isDNPartOfBase') - ->willReturn(true); - - $access->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['displayName', 'mail']); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $users = $groupBackend->countUsersInGroup('foobar'); - - $this->assertSame(4, $users); - } - - public function testGetUserGroupsMemberOf() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $this->enableGroups($access); - - $dn = 'cn=userX,dc=foobar'; - - $access->connection->hasPrimaryGroups = false; - $access->connection->hasGidNumber = false; - - $access->expects($this->any()) - ->method('username2dn') - ->willReturn($dn); - $access->expects($this->exactly(5)) - ->method('readAttribute') - ->will($this->onConsecutiveCalls(['cn=groupA,dc=foobar', 'cn=groupB,dc=foobar'], [], [], [], [])); - $access->expects($this->any()) - ->method('dn2groupname') - ->willReturnArgument(0); - $access->expects($this->any()) - ->method('groupname2dn') - ->willReturnArgument(0); - $access->expects($this->any()) - ->method('isDNPartOfBase') - ->willReturn(true); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $groups = $groupBackend->getUserGroups('userX'); - - $this->assertSame(2, count($groups)); - } - - public function testGetUserGroupsMemberOfDisabled() { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $access->connection = $this->createMock(Connection::class); - $access->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'useMemberOfToDetectMembership') { - return 0; - } elseif ($name === 'ldapDynamicGroupMemberURL') { - return ''; - } - return 1; - }); - - $dn = 'cn=userX,dc=foobar'; - - $access->connection->hasPrimaryGroups = false; - $access->connection->hasGidNumber = false; - - $access->expects($this->once()) - ->method('username2dn') - ->willReturn($dn); - $access->expects($this->never()) - ->method('readAttribute') - ->with($dn, 'memberOf'); - $access->expects($this->once()) - ->method('nextcloudGroupNames') - ->willReturn([]); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $groupBackend->getUserGroups('userX'); - } - - public function nestedGroupsProvider(): array { - return [ - [true], - [false], - ]; - } - - /** - * @dataProvider nestedGroupsProvider - */ - public function testGetGroupsByMember(bool $nestedGroups) { - $access = $this->getAccessMock(); - $pluginManager = $this->getPluginManagerMock(); - - $groupFilter = '(&(objectclass=nextcloudGroup)(nextcloudEnabled=TRUE))'; - $access->connection = $this->createMock(Connection::class); - $access->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) use ($nestedGroups, $groupFilter) { - switch ($name) { - case 'useMemberOfToDetectMembership': - return 0; - case 'ldapDynamicGroupMemberURL': - return ''; - case 'ldapNestedGroups': - return $nestedGroups; - case 'ldapGroupMemberAssocAttr': - return 'member'; - case 'ldapGroupFilter': - return $groupFilter; - } - return 1; - }); - - $dn = 'cn=userX,dc=foobar'; - - $access->connection->hasPrimaryGroups = false; - $access->connection->hasGidNumber = false; - - $access->expects($this->exactly(2)) - ->method('username2dn') - ->willReturn($dn); - $access->expects($this->any()) - ->method('readAttribute') - ->willReturn([]); - $access->expects($this->any()) - ->method('combineFilterWithAnd') - ->willReturnCallback(function (array $filterParts) { - // ⚠ returns a pseudo-filter only, not real LDAP Filter syntax - return implode('&', $filterParts); - }); - - $group1 = [ - 'cn' => 'group1', - 'dn' => ['cn=group1,ou=groups,dc=domain,dc=com'], - ]; - $group2 = [ - 'cn' => 'group2', - 'dn' => ['cn=group2,ou=groups,dc=domain,dc=com'], - ]; - - $access->expects($this->once()) - ->method('nextcloudGroupNames') - ->with([$group1, $group2]) - ->willReturn(['group1', 'group2']); - $access->expects($nestedGroups ? $this->atLeastOnce() : $this->once()) - ->method('fetchListOfGroups') - ->willReturnCallback(function ($filter, $attr, $limit, $offset) use ($nestedGroups, $groupFilter, $group1, $group2) { - static $firstRun = true; - if (!$nestedGroups) { - // When nested groups are enabled, groups cannot be filtered early as it would - // exclude intermediate groups. But we can, and should, when working with flat groups. - $this->assertTrue(strpos($filter, $groupFilter) !== false); - } - if ($firstRun) { - $firstRun = false; - return [$group1, $group2]; - } - return []; - }); - $access->expects($this->any()) - ->method('dn2groupname') - ->willReturnCallback(function (string $dn) { - return ldap_explode_dn($dn, 1)[0]; - }); - $access->expects($this->any()) - ->method('groupname2dn') - ->willReturnCallback(function (string $gid) use ($group1, $group2) { - if ($gid === $group1['cn']) { - return $group1['dn'][0]; - } - if ($gid === $group2['cn']) { - return $group2['dn'][0]; - } - }); - $access->expects($this->any()) - ->method('isDNPartOfBase') - ->willReturn(true); - - $groupBackend = new GroupLDAP($access, $pluginManager); - $groups = $groupBackend->getUserGroups('userX'); - $this->assertEquals(['group1', 'group2'], $groups); - - $groupsAgain = $groupBackend->getUserGroups('userX'); - $this->assertEquals(['group1', 'group2'], $groupsAgain); - } - - public function testCreateGroupWithPlugin() { - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'createGroup']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::CREATE_GROUP) - ->willReturn(true); - - $pluginManager->expects($this->once()) - ->method('createGroup') - ->with('gid') - ->willReturn('result'); - - $access = $this->getAccessMock(); - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $this->assertEquals($ldap->createGroup('gid'), true); - } - - - public function testCreateGroupFailing() { - $this->expectException(\Exception::class); - - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'createGroup']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::CREATE_GROUP) - ->willReturn(false); - - $access = $this->getAccessMock(); - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $ldap->createGroup('gid'); - } - - public function testDeleteGroupWithPlugin() { - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'deleteGroup']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::DELETE_GROUP) - ->willReturn(true); - - $pluginManager->expects($this->once()) - ->method('deleteGroup') - ->with('gid') - ->willReturn('result'); - - $mapper = $this->getMockBuilder(GroupMapping::class) - ->setMethods(['unmap']) - ->disableOriginalConstructor() - ->getMock(); - - $access = $this->getAccessMock(); - $access->expects($this->any()) - ->method('getGroupMapper') - ->willReturn($mapper); - - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $this->assertEquals($ldap->deleteGroup('gid'), 'result'); - } - - - public function testDeleteGroupFailing() { - $this->expectException(\Exception::class); - - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'deleteGroup']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::DELETE_GROUP) - ->willReturn(false); - - $access = $this->getAccessMock(); - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $ldap->deleteGroup('gid'); - } - - public function testAddToGroupWithPlugin() { - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'addToGroup']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::ADD_TO_GROUP) - ->willReturn(true); - - $pluginManager->expects($this->once()) - ->method('addToGroup') - ->with('uid', 'gid') - ->willReturn('result'); - - $access = $this->getAccessMock(); - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $this->assertEquals($ldap->addToGroup('uid', 'gid'), 'result'); - } - - - public function testAddToGroupFailing() { - $this->expectException(\Exception::class); - - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'addToGroup']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::ADD_TO_GROUP) - ->willReturn(false); - - $access = $this->getAccessMock(); - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $ldap->addToGroup('uid', 'gid'); - } - - public function testRemoveFromGroupWithPlugin() { - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'removeFromGroup']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::REMOVE_FROM_GROUP) - ->willReturn(true); - - $pluginManager->expects($this->once()) - ->method('removeFromGroup') - ->with('uid', 'gid') - ->willReturn('result'); - - $access = $this->getAccessMock(); - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $this->assertEquals($ldap->removeFromGroup('uid', 'gid'), 'result'); - } - - - public function testRemoveFromGroupFailing() { - $this->expectException(\Exception::class); - - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'removeFromGroup']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::REMOVE_FROM_GROUP) - ->willReturn(false); - - $access = $this->getAccessMock(); - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $ldap->removeFromGroup('uid', 'gid'); - } - - public function testGetGroupDetailsWithPlugin() { - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'getGroupDetails']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::GROUP_DETAILS) - ->willReturn(true); - - $pluginManager->expects($this->once()) - ->method('getGroupDetails') - ->with('gid') - ->willReturn('result'); - - $access = $this->getAccessMock(); - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $this->assertEquals($ldap->getGroupDetails('gid'), 'result'); - } - - - public function testGetGroupDetailsFailing() { - $this->expectException(\Exception::class); - - /** @var GroupPluginManager|MockObject $pluginManager */ - $pluginManager = $this->getMockBuilder(GroupPluginManager::class) - ->setMethods(['implementsActions', 'getGroupDetails']) - ->getMock(); - - $pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(GroupInterface::GROUP_DETAILS) - ->willReturn(false); - - $access = $this->getAccessMock(); - $access->connection = $this->createMock(Connection::class); - - $ldap = new GroupLDAP($access, $pluginManager); - - $ldap->getGroupDetails('gid'); - } - - public function groupMemberProvider() { - $base = 'dc=species,dc=earth'; - - $groups0 = [ - 'uid=3723,' . $base, - 'uid=8372,' . $base, - 'uid=8427,' . $base, - 'uid=2333,' . $base, - 'uid=4754,' . $base, - ]; - $groups1 = [ - '3723', - '8372', - '8427', - '2333', - '4754', - ]; - $groups2Nested = ['6642', '1424']; - $expGroups2 = array_merge($groups1, $groups2Nested); - - return [ - [ #0 – test DNs - 'cn=Birds,' . $base, - $groups0, - ['cn=Birds,' . $base => $groups0] - ], - [ #1 – test uids - 'cn=Birds,' . $base, - $groups1, - ['cn=Birds,' . $base => $groups1] - ], - ]; - } - - /** - * @param string $groupDN - * @param string[] $expectedMembers - * @param array $groupsInfo - * @dataProvider groupMemberProvider - */ - public function testGroupMembers($groupDN, $expectedMembers, $groupsInfo = null) { - $access = $this->getAccessMock(); - $access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($group) use ($groupDN, $expectedMembers, $groupsInfo) { - if (isset($groupsInfo[$group])) { - return $groupsInfo[$group]; - } - return []; - }); - - $access->connection = $this->createMock(Connection::class); - if (count($groupsInfo) > 1) { - $access->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapNestedGroups') { - return 1; - } - return null; - }); - } - - /** @var GroupPluginManager $pluginManager */ - $pluginManager = $this->createMock(GroupPluginManager::class); - - $ldap = new GroupLDAP($access, $pluginManager); - $resultingMembers = $this->invokePrivate($ldap, '_groupMembers', [$groupDN]); - - $this->assertEqualsCanonicalizing($expectedMembers, $resultingMembers); - } - - public function displayNameProvider() { - return [ - ['Graphic Novelists', ['Graphic Novelists']], - ['', false], - ]; - } - - /** - * @dataProvider displayNameProvider - */ - public function testGetDisplayName(string $expected, $ldapResult) { - $gid = 'graphic_novelists'; - - $access = $this->getAccessMock(); - $access->expects($this->atLeastOnce()) - ->method('readAttribute') - ->willReturn($ldapResult); - - $access->connection = $this->createMock(Connection::class); - $access->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapGroupMemberAssocAttr') { - return 'member'; - } elseif ($name === 'ldapGroupFilter') { - return 'objectclass=nextcloudGroup'; - } elseif ($name === 'ldapGroupDisplayName') { - return 'cn'; - } - return null; - }); - - /** @var GroupPluginManager $pluginManager */ - $pluginManager = $this->createMock(GroupPluginManager::class); - - $ldap = new GroupLDAP($access, $pluginManager); - $this->assertSame($expected, $ldap->getDisplayName($gid)); - } -} diff --git a/apps/user_ldap/tests/HelperTest.php b/apps/user_ldap/tests/HelperTest.php deleted file mode 100644 index 1822c27a1cdd8..0000000000000 --- a/apps/user_ldap/tests/HelperTest.php +++ /dev/null @@ -1,117 +0,0 @@ - - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\Helper; -use OCP\IConfig; - -class HelperTest extends \Test\TestCase { - - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - private $config; - - /** @var Helper */ - private $helper; - - protected function setUp(): void { - parent::setUp(); - - $this->config = $this->createMock(IConfig::class); - $this->helper = new Helper($this->config); - } - - public function testGetServerConfigurationPrefixes() { - $this->config->method('getAppKeys') - ->with($this->equalTo('user_ldap')) - ->willReturn([ - 'foo', - 'ldap_configuration_active', - 's1ldap_configuration_active', - ]); - - $result = $this->helper->getServerConfigurationPrefixes(false); - - $this->assertEquals(['', 's1'], $result); - } - - public function testGetServerConfigurationPrefixesActive() { - $this->config->method('getAppKeys') - ->with($this->equalTo('user_ldap')) - ->willReturn([ - 'foo', - 'ldap_configuration_active', - 's1ldap_configuration_active', - ]); - - $this->config->method('getAppValue') - ->willReturnCallback(function ($app, $key, $default) { - if ($app !== 'user_ldap') { - $this->fail('wrong app'); - } - if ($key === 's1ldap_configuration_active') { - return '1'; - } - return $default; - }); - - $result = $this->helper->getServerConfigurationPrefixes(true); - - $this->assertEquals(['s1'], $result); - } - - public function testGetServerConfigurationHost() { - $this->config->method('getAppKeys') - ->with($this->equalTo('user_ldap')) - ->willReturn([ - 'foo', - 'ldap_host', - 's1ldap_host', - 's02ldap_host', - ]); - - $this->config->method('getAppValue') - ->willReturnCallback(function ($app, $key, $default) { - if ($app !== 'user_ldap') { - $this->fail('wrong app'); - } - if ($key === 'ldap_host') { - return 'example.com'; - } - if ($key === 's1ldap_host') { - return 'foo.bar.com'; - } - return $default; - }); - - $result = $this->helper->getServerConfigurationHosts(); - - $this->assertEquals([ - '' => 'example.com', - 's1' => 'foo.bar.com', - 's02' => '', - ], $result); - } -} diff --git a/apps/user_ldap/tests/Integration/AbstractIntegrationTest.php b/apps/user_ldap/tests/Integration/AbstractIntegrationTest.php deleted file mode 100644 index acca987a81d6c..0000000000000 --- a/apps/user_ldap/tests/Integration/AbstractIntegrationTest.php +++ /dev/null @@ -1,180 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Morris Jobke - * @author Roeland Jago Douma - * @author root - * @author Vinicius Cubas Brand - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Integration; - -use OCA\User_LDAP\Access; -use OCA\User_LDAP\Connection; -use OCA\User_LDAP\FilesystemHelper; -use OCA\User_LDAP\GroupPluginManager; -use OCA\User_LDAP\Helper; -use OCA\User_LDAP\LDAP; -use OCA\User_LDAP\LogWrapper; -use OCA\User_LDAP\User\Manager; -use OCA\User_LDAP\UserPluginManager; - -abstract class AbstractIntegrationTest { - /** @var LDAP */ - protected $ldap; - - /** @var Connection */ - protected $connection; - - /** @var Access */ - protected $access; - - /** @var Manager */ - protected $userManager; - - /** @var Helper */ - protected $helper; - - /** @var string */ - protected $base; - - /** @var string[] */ - protected $server; - - public function __construct($host, $port, $bind, $pwd, $base) { - $this->base = $base; - $this->server = [ - 'host' => $host, - 'port' => $port, - 'dn' => $bind, - 'pwd' => $pwd - ]; - } - - /** - * prepares the LDAP environment and sets up a test configuration for - * the LDAP backend. - */ - public function init() { - \OC::$server->registerService(UserPluginManager::class, function () { - return new \OCA\User_LDAP\UserPluginManager(); - }); - \OC::$server->registerService(GroupPluginManager::class, function () { - return new \OCA\User_LDAP\GroupPluginManager(); - }); - - $this->initLDAPWrapper(); - $this->initConnection(); - $this->initUserManager(); - $this->initHelper(); - $this->initAccess(); - } - - /** - * initializes the test LDAP wrapper - */ - protected function initLDAPWrapper() { - $this->ldap = new LDAP(); - } - - /** - * sets up the LDAP configuration to be used for the test - */ - protected function initConnection() { - $this->connection = new Connection($this->ldap, '', null); - $this->connection->setConfiguration([ - 'ldapHost' => $this->server['host'], - 'ldapPort' => $this->server['port'], - 'ldapBase' => $this->base, - 'ldapAgentName' => $this->server['dn'], - 'ldapAgentPassword' => $this->server['pwd'], - 'ldapUserFilter' => 'objectclass=inetOrgPerson', - 'ldapUserDisplayName' => 'cn', - 'ldapGroupDisplayName' => 'cn', - 'ldapLoginFilter' => '(|(uid=%uid)(samaccountname=%uid))', - 'ldapCacheTTL' => 0, - 'ldapConfigurationActive' => 1, - ]); - } - - /** - * initializes an LDAP user manager instance - */ - protected function initUserManager() { - $this->userManager = new Manager( - \OC::$server->getConfig(), - new FilesystemHelper(), - new LogWrapper(), - \OC::$server->getAvatarManager(), - new \OCP\Image(), - \OC::$server->getDatabaseConnection(), - \OC::$server->getUserManager(), - \OC::$server->getNotificationManager() - ); - } - - /** - * initializes the test Helper - */ - protected function initHelper() { - $this->helper = new Helper(\OC::$server->getConfig()); - } - - /** - * initializes the Access test instance - */ - protected function initAccess() { - $this->access = new Access($this->connection, $this->ldap, $this->userManager, $this->helper, \OC::$server->getConfig(), \OC::$server->getLogger()); - } - - /** - * runs the test cases while outputting progress and result information - * - * If a test failed, the script is exited with return code 1. - */ - public function run() { - $methods = get_class_methods($this); - $atLeastOneCaseRan = false; - foreach ($methods as $method) { - if (strpos($method, 'case') === 0) { - print("running $method " . PHP_EOL); - try { - if (!$this->$method()) { - print(PHP_EOL . '>>> !!! Test ' . $method . ' FAILED !!! <<<' . PHP_EOL . PHP_EOL); - exit(1); - } - $atLeastOneCaseRan = true; - } catch (\Exception $e) { - print(PHP_EOL . '>>> !!! Test ' . $method . ' RAISED AN EXCEPTION !!! <<<' . PHP_EOL); - print($e->getMessage() . PHP_EOL . PHP_EOL); - exit(1); - } - } - } - if ($atLeastOneCaseRan) { - print('Tests succeeded' . PHP_EOL); - } else { - print('No Test was available.' . PHP_EOL); - exit(1); - } - } -} diff --git a/apps/user_ldap/tests/Integration/Bootstrap.php b/apps/user_ldap/tests/Integration/Bootstrap.php deleted file mode 100644 index afa92dac958f1..0000000000000 --- a/apps/user_ldap/tests/Integration/Bootstrap.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * @author Arthur Schiwon - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -define('CLI_TEST_RUN', true); -require_once __DIR__ . '/../../../../lib/base.php'; -require_once __DIR__ . '/setup-scripts/config.php'; diff --git a/apps/user_ldap/tests/Integration/ExceptionOnLostConnection.php b/apps/user_ldap/tests/Integration/ExceptionOnLostConnection.php deleted file mode 100644 index e78208e579c75..0000000000000 --- a/apps/user_ldap/tests/Integration/ExceptionOnLostConnection.php +++ /dev/null @@ -1,198 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Integration; - -use OC\ServerNotAvailableException; -use OCA\User_LDAP\LDAP; - -/** - * Class ExceptionOnLostConnection - * - * integration test, ensures that an exception is thrown, when the connection is lost. - * - * LDAP must be available via toxiproxy. - * - * This test must be run manually. - * - */ -class ExceptionOnLostConnection { - /** @var string */ - private $toxiProxyHost; - - /** @var string */ - private $toxiProxyName; - - /** @var string */ - private $ldapBase; - - /** @var string|null */ - private $ldapBindDN; - - /** @var string|null */ - private $ldapBindPwd; - - /** @var string */ - private $ldapHost; - - /** @var \OCA\User_LDAP\LDAP */ - private $ldap; - - /** @var bool */ - private $originalProxyState; - - /** - * @param string $proxyHost host of toxiproxy as url, like http://localhost:8474 - * @param string $proxyName name of the LDAP proxy service as configured in toxiProxy - * @param string $ldapBase any valid LDAP base DN - * @param null $bindDN optional, bind DN if anonymous bind is not possible - * @param null $bindPwd optional - */ - public function __construct($proxyHost, $proxyName, $ldapBase, $bindDN = null, $bindPwd = null) { - $this->toxiProxyHost = $proxyHost; - $this->toxiProxyName = $proxyName; - $this->ldapBase = $ldapBase; - $this->ldapBindDN = $bindDN; - $this->ldapBindPwd = $bindPwd; - - $this->setUp(); - } - - /** - * destructor - */ - public function __destruct() { - $this->cleanUp(); - } - - /** - * prepares everything for the test run. Includes loading Nextcloud and - * the LDAP backend, as well as getting information about toxiproxy. - * Also creates an instance of the LDAP class, the testee - * - * @throws \Exception - */ - public function setUp(): void { - require_once __DIR__ . '/../../../../lib/base.php'; - \OC_App::loadApps(['user_ldap']); - - $ch = $this->getCurl(); - $proxyInfoJson = curl_exec($ch); - $this->checkCurlResult($ch, $proxyInfoJson); - $proxyInfo = json_decode($proxyInfoJson, true); - $this->originalProxyState = $proxyInfo['enabled']; - $this->ldapHost = 'ldap://' . $proxyInfo['listen']; // contains port as well - - $this->ldap = new LDAP(); - } - - /** - * restores original state of the LDAP proxy, if necessary - */ - public function cleanUp() { - if ($this->originalProxyState === true) { - $this->setProxyState(true); - } - } - - /** - * runs the test and prints the result. Exit code is 0 if successful, 1 on - * fail - */ - public function run() { - if ($this->originalProxyState === false) { - $this->setProxyState(true); - } - //host contains port, 2nd parameter will be ignored - $cr = $this->ldap->connect($this->ldapHost, 0); - $this->ldap->bind($cr, $this->ldapBindDN, $this->ldapBindPwd); - $this->ldap->search($cr, $this->ldapBase, 'objectClass=*', ['dn'], true, 5); - - // disable LDAP, will cause lost connection - $this->setProxyState(false); - try { - $this->ldap->search($cr, $this->ldapBase, 'objectClass=*', ['dn'], true, 5); - } catch (ServerNotAvailableException $e) { - print("Test PASSED" . PHP_EOL); - exit(0); - } - print("Test FAILED" . PHP_EOL); - exit(1); - } - - /** - * tests whether a curl operation ran successfully. If not, an exception - * is thrown - * - * @param resource $ch - * @param mixed $result - * @throws \Exception - */ - private function checkCurlResult($ch, $result) { - if ($result === false) { - $error = curl_error($ch); - curl_close($ch); - throw new \Exception($error); - } - } - - /** - * enables or disabled the LDAP proxy service in toxiproxy - * - * @param bool $isEnabled whether is should be enabled or disables - * @throws \Exception - */ - private function setProxyState($isEnabled) { - if (!is_bool($isEnabled)) { - throw new \InvalidArgumentException('Bool expected'); - } - $postData = json_encode(['enabled' => $isEnabled]); - $ch = $this->getCurl(); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'Content-Length: ' . strlen($postData)] - ); - $recvd = curl_exec($ch); - $this->checkCurlResult($ch, $recvd); - } - - /** - * initializes a curl handler towards the toxiproxy LDAP proxy service - * @return resource - */ - private function getCurl() { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $this->toxiProxyHost . '/proxies/' . $this->toxiProxyName); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - return $ch; - } -} - -$test = new ExceptionOnLostConnection('http://localhost:8474', 'ldap', 'dc=owncloud,dc=bzoc'); -$test->run(); diff --git a/apps/user_ldap/tests/Integration/Lib/IntegrationTestAttributeDetection.php b/apps/user_ldap/tests/Integration/Lib/IntegrationTestAttributeDetection.php deleted file mode 100644 index 5669e2164b724..0000000000000 --- a/apps/user_ldap/tests/Integration/Lib/IntegrationTestAttributeDetection.php +++ /dev/null @@ -1,89 +0,0 @@ - - * - * @author Arthur Schiwon - * @author Morris Jobke - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\user_ldap\tests\Integration\Lib; - -use OCA\User_LDAP\Group_LDAP; -use OCA\User_LDAP\GroupPluginManager; -use OCA\User_LDAP\Mapping\GroupMapping; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\Tests\Integration\AbstractIntegrationTest; -use OCA\User_LDAP\User_LDAP; -use OCA\User_LDAP\UserPluginManager; - -require_once __DIR__ . '/../Bootstrap.php'; - -class IntegrationTestAttributeDetection extends AbstractIntegrationTest { - public function init() { - require(__DIR__ . '/../setup-scripts/createExplicitUsers.php'); - require(__DIR__ . '/../setup-scripts/createExplicitGroups.php'); - - parent::init(); - - $this->connection->setConfiguration(['ldapGroupFilter' => 'objectClass=groupOfNames']); - $this->connection->setConfiguration(['ldapGroupMemberAssocAttr' => 'member']); - - $userMapper = new UserMapping(\OC::$server->getDatabaseConnection()); - $userMapper->clear(); - $this->access->setUserMapper($userMapper); - - $groupMapper = new GroupMapping(\OC::$server->getDatabaseConnection()); - $groupMapper->clear(); - $this->access->setGroupMapper($groupMapper); - - $userBackend = new User_LDAP($this->access, \OC::$server->getConfig(), \OC::$server->getNotificationManager(), \OC::$server->getUserSession(), \OC::$server->query(UserPluginManager::class)); - $userManager = \OC::$server->getUserManager(); - $userManager->clearBackends(); - $userManager->registerBackend($userBackend); - - $groupBackend = new Group_LDAP($this->access, \OC::$server->query(GroupPluginManager::class)); - $groupManger = \OC::$server->getGroupManager(); - $groupManger->clearBackends(); - $groupManger->addBackend($groupBackend); - } - - protected function caseNativeUUIDAttributeUsers() { - // trigger importing of users which also triggers UUID attribute detection - \OC::$server->getUserManager()->search('', 5, 0); - return $this->connection->ldapUuidUserAttribute === 'entryuuid'; - } - - protected function caseNativeUUIDAttributeGroups() { - // essentially the same as 'caseNativeUUIDAttributeUsers', code paths - // are similar, but we take no chances. - - // trigger importing of users which also triggers UUID attribute detection - \OC::$server->getGroupManager()->search('', 5, 0); - return $this->connection->ldapUuidGroupAttribute === 'entryuuid'; - } -} - -/** @var string $host */ -/** @var int $port */ -/** @var string $adn */ -/** @var string $apwd */ -/** @var string $bdn */ -$test = new IntegrationTestAttributeDetection($host, $port, $adn, $apwd, $bdn); -$test->init(); -$test->run(); diff --git a/apps/user_ldap/tests/Integration/Lib/IntegrationTestCountUsersByLoginName.php b/apps/user_ldap/tests/Integration/Lib/IntegrationTestCountUsersByLoginName.php deleted file mode 100644 index dc483ebec6d34..0000000000000 --- a/apps/user_ldap/tests/Integration/Lib/IntegrationTestCountUsersByLoginName.php +++ /dev/null @@ -1,71 +0,0 @@ - - * @author Joas Schilling - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Integration\Lib; - -use OCA\User_LDAP\Tests\Integration\AbstractIntegrationTest; - -require_once __DIR__ . '/../Bootstrap.php'; - -class IntegrationTestCountUsersByLoginName extends AbstractIntegrationTest { - - /** - * prepares the LDAP environment and sets up a test configuration for - * the LDAP backend. - */ - public function init() { - require(__DIR__ . '/../setup-scripts/createExplicitUsers.php'); - parent::init(); - } - - /** - * tests countUsersByLoginName where it is expected that the login name does - * not match any LDAP user - * - * @return bool - */ - protected function case1() { - $result = $this->access->countUsersByLoginName('nothere'); - return $result === 0; - } - - /** - * tests countUsersByLoginName where it is expected that the login name does - * match one LDAP user - * - * @return bool - */ - protected function case2() { - $result = $this->access->countUsersByLoginName('alice'); - return $result === 1; - } -} - -/** @var string $host */ -/** @var int $port */ -/** @var string $adn */ -/** @var string $apwd */ -/** @var string $bdn */ -$test = new IntegrationTestCountUsersByLoginName($host, $port, $adn, $apwd, $bdn); -$test->init(); -$test->run(); diff --git a/apps/user_ldap/tests/Integration/Lib/IntegrationTestFetchUsersByLoginName.php b/apps/user_ldap/tests/Integration/Lib/IntegrationTestFetchUsersByLoginName.php deleted file mode 100644 index 7fc109966404d..0000000000000 --- a/apps/user_ldap/tests/Integration/Lib/IntegrationTestFetchUsersByLoginName.php +++ /dev/null @@ -1,86 +0,0 @@ - - * @author Joas Schilling - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Integration\Lib; - -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\Tests\Integration\AbstractIntegrationTest; -use OCA\User_LDAP\User_LDAP; -use OCA\User_LDAP\UserPluginManager; - -require_once __DIR__ . '/../Bootstrap.php'; - -class IntegrationTestFetchUsersByLoginName extends AbstractIntegrationTest { - /** @var UserMapping */ - protected $mapping; - - /** @var User_LDAP */ - protected $backend; - - /** - * prepares the LDAP environment and sets up a test configuration for - * the LDAP backend. - */ - public function init() { - require(__DIR__ . '/../setup-scripts/createExplicitUsers.php'); - parent::init(); - - $this->mapping = new UserMapping(\OC::$server->getDatabaseConnection()); - $this->mapping->clear(); - $this->access->setUserMapper($this->mapping); - $this->backend = new User_LDAP($this->access, \OC::$server->getConfig(), \OC::$server->getNotificationManager(), \OC::$server->getUserSession(), \OC::$server->query(UserPluginManager::class)); - } - - /** - * tests fetchUserByLoginName where it is expected that the login name does - * not match any LDAP user - * - * @return bool - */ - protected function case1() { - $result = $this->access->fetchUsersByLoginName('nothere'); - return $result === []; - } - - /** - * tests fetchUserByLoginName where it is expected that the login name does - * match one LDAP user - * - * @return bool - */ - protected function case2() { - $result = $this->access->fetchUsersByLoginName('alice'); - return count($result) === 1; - } -} - -/** @var string $host */ -/** @var int $port */ -/** @var string $adn */ -/** @var string $apwd */ -/** @var string $bdn */ -$test = new IntegrationTestFetchUsersByLoginName($host, $port, $adn, $apwd, $bdn); -$test->init(); -$test->run(); diff --git a/apps/user_ldap/tests/Integration/Lib/IntegrationTestPaging.php b/apps/user_ldap/tests/Integration/Lib/IntegrationTestPaging.php deleted file mode 100644 index 066c91a149baa..0000000000000 --- a/apps/user_ldap/tests/Integration/Lib/IntegrationTestPaging.php +++ /dev/null @@ -1,99 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Integration\Lib; - -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\Tests\Integration\AbstractIntegrationTest; -use OCA\User_LDAP\User_LDAP; -use OCA\User_LDAP\UserPluginManager; - -require_once __DIR__ . '/../Bootstrap.php'; - -class IntegrationTestPaging extends AbstractIntegrationTest { - /** @var UserMapping */ - protected $mapping; - - /** @var User_LDAP */ - protected $backend; - - /** @var int */ - protected $pagingSize = 2; - - /** - * prepares the LDAP environment and sets up a test configuration for - * the LDAP backend. - */ - public function init() { - require(__DIR__ . '/../setup-scripts/createExplicitUsers.php'); - parent::init(); - - $this->backend = new User_LDAP($this->access, \OC::$server->getConfig(), \OC::$server->getNotificationManager(), \OC::$server->getUserSession(), \OC::$server->query(UserPluginManager::class)); - } - - public function initConnection() { - parent::initConnection(); - $this->connection->setConfiguration([ - 'ldapPagingSize' => $this->pagingSize - ]); - } - - /** - * fetch first three, afterwards all users - * - * @return bool - */ - protected function case1() { - $filter = 'objectclass=inetorgperson'; - $attributes = ['cn', 'dn']; - - $result = $this->access->searchUsers($filter, $attributes, 4); - // beware, under circumstances, the result set can be larger then - // the specified limit! In this case, if we specify a limit of 3, - // the result will be 4, because the highest possible paging size - // is 2 (as configured). - // But also with more than one search base, the limit can be outpaced. - if (count($result) !== 4) { - return false; - } - - $result = $this->access->searchUsers($filter, $attributes); - if (count($result) !== 7) { - return false; - } - - return true; - } -} - -/** @var string $host */ -/** @var int $port */ -/** @var string $adn */ -/** @var string $apwd */ -/** @var string $bdn */ -$test = new IntegrationTestPaging($host, $port, $adn, $apwd, $bdn); -$test->init(); -$test->run(); diff --git a/apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserAvatar.php b/apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserAvatar.php deleted file mode 100644 index 8c0bf6cde864c..0000000000000 --- a/apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserAvatar.php +++ /dev/null @@ -1,168 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Morris Jobke - * @author Roeland Jago Douma - * @author Roger Szabo - * @author Thomas Müller - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Integration\Lib\User; - -use OCA\User_LDAP\FilesystemHelper; -use OCA\User_LDAP\LogWrapper; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\Tests\Integration\AbstractIntegrationTest; -use OCA\User_LDAP\User\Manager; -use OCA\User_LDAP\User\User; -use OCA\User_LDAP\User_LDAP; -use OCA\User_LDAP\UserPluginManager; -use OCP\Image; - -require_once __DIR__ . '/../../Bootstrap.php'; - -class IntegrationTestUserAvatar extends AbstractIntegrationTest { - /** @var UserMapping */ - protected $mapping; - - /** - * prepares the LDAP environment and sets up a test configuration for - * the LDAP backend. - */ - public function init() { - require(__DIR__ . '/../../setup-scripts/createExplicitUsers.php'); - parent::init(); - $this->mapping = new UserMapping(\OC::$server->getDatabaseConnection()); - $this->mapping->clear(); - $this->access->setUserMapper($this->mapping); - $userBackend = new User_LDAP($this->access, \OC::$server->getConfig(), \OC::$server->getNotificationManager(), \OC::$server->getUserSession(), \OC::$server->query(UserPluginManager::class)); - \OC_User::useBackend($userBackend); - } - - /** - * A method that does the common steps of test cases 1 and 2. The evaluation - * is not happening here. - * - * @param string $dn - * @param string $username - * @param string $image - */ - private function execFetchTest($dn, $username, $image) { - $this->setJpegPhotoAttribute($dn, $image); - - // assigns our self-picked oc username to the dn - $this->mapping->map($dn, $username, 'fakeUUID-' . $username); - - // initialize home folder and make sure that the user will update - // also remove an possibly existing avatar - \OC_Util::tearDownFS(); - \OC_Util::setupFS($username); - \OC::$server->getUserFolder($username); - \OC::$server->getConfig()->deleteUserValue($username, 'user_ldap', User::USER_PREFKEY_LASTREFRESH); - if (\OC::$server->getAvatarManager()->getAvatar($username)->exists()) { - \OC::$server->getAvatarManager()->getAvatar($username)->remove(); - } - - // finally attempt to get the avatar set - $user = $this->userManager->get($dn); - $user->updateAvatar(); - } - - /** - * tests whether an avatar can be retrieved from LDAP and stored correctly - * - * @return bool - */ - protected function case1() { - $image = file_get_contents(__DIR__ . '/../../data/avatar-valid.jpg'); - $dn = 'uid=alice,ou=Users,' . $this->base; - $username = 'alice1337'; - - $this->execFetchTest($dn, $username, $image); - - return \OC::$server->getAvatarManager()->getAvatar($username)->exists(); - } - - /** - * tests whether an image received from LDAP which is of an invalid file - * type is dealt with properly (i.e. not set and not dying). - * - * @return bool - */ - protected function case2() { - // gif by Pmspinner from https://commons.wikimedia.org/wiki/File:Avatar2469_3.gif - $image = file_get_contents(__DIR__ . '/../../data/avatar-invalid.gif'); - $dn = 'uid=boris,ou=Users,' . $this->base; - $username = 'boris7844'; - - $this->execFetchTest($dn, $username, $image); - - return !\OC::$server->getAvatarManager()->getAvatar($username)->exists(); - } - - /** - * This writes an image to the 'jpegPhoto' attribute on LDAP. - * - * @param string $dn - * @param string $image An image read via file_get_contents - * @throws \OC\ServerNotAvailableException - */ - private function setJpegPhotoAttribute($dn, $image) { - $changeSet = ['jpegphoto' => $image]; - ldap_mod_add($this->connection->getConnectionResource(), $dn, $changeSet); - } - - protected function initUserManager() { - $this->userManager = new Manager( - \OC::$server->getConfig(), - new FilesystemHelper(), - new LogWrapper(), - \OC::$server->getAvatarManager(), - new Image(), - \OC::$server->getDatabaseConnection(), - \OC::$server->getUserManager(), - \OC::$server->getNotificationManager() - ); - } - - /** - * sets up the LDAP configuration to be used for the test - */ - protected function initConnection() { - parent::initConnection(); - $this->connection->setConfiguration([ - 'ldapUserFilter' => 'objectclass=inetOrgPerson', - 'ldapUserDisplayName' => 'displayName', - 'ldapGroupDisplayName' => 'cn', - 'ldapLoginFilter' => 'uid=%uid', - ]); - } -} - -/** @var string $host */ -/** @var int $port */ -/** @var string $adn */ -/** @var string $apwd */ -/** @var string $bdn */ -$test = new IntegrationTestUserAvatar($host, $port, $adn, $apwd, $bdn); -$test->init(); -$test->run(); diff --git a/apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserCleanUp.php b/apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserCleanUp.php deleted file mode 100644 index c48f51368bc6f..0000000000000 --- a/apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserCleanUp.php +++ /dev/null @@ -1,105 +0,0 @@ - - * @author Christoph Wurst - * @author Morris Jobke - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Integration\Lib\User; - -use OCA\User_LDAP\Jobs\CleanUp; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\Tests\Integration\AbstractIntegrationTest; -use OCA\User_LDAP\User_LDAP; -use OCA\User_LDAP\UserPluginManager; - -require_once __DIR__ . '/../../Bootstrap.php'; - -class IntegrationTestUserCleanUp extends AbstractIntegrationTest { - /** @var UserMapping */ - protected $mapping; - - /** - * prepares the LDAP environment and sets up a test configuration for - * the LDAP backend. - */ - public function init() { - require(__DIR__ . '/../../setup-scripts/createExplicitUsers.php'); - parent::init(); - $this->mapping = new UserMapping(\OC::$server->getDatabaseConnection()); - $this->mapping->clear(); - $this->access->setUserMapper($this->mapping); - - $userBackend = new User_LDAP($this->access, \OC::$server->getConfig(), \OC::$server->getNotificationManager(), \OC::$server->getUserSession(), \OC::$server->query(UserPluginManager::class)); - \OC_User::useBackend($userBackend); - } - - /** - * adds a map entry for the user, so we know the username - * - * @param $dn - * @param $username - */ - private function prepareUser($dn, $username) { - // assigns our self-picked oc username to the dn - $this->mapping->map($dn, $username, 'fakeUUID-' . $username); - } - - private function deleteUserFromLDAP($dn) { - $cr = $this->connection->getConnectionResource(); - ldap_delete($cr, $dn); - } - - /** - * tests whether a display name consisting of two parts is created correctly - * - * @return bool - */ - protected function case1() { - $username = 'alice1337'; - $dn = 'uid=alice,ou=Users,' . $this->base; - $this->prepareUser($dn, $username); - - $this->deleteUserFromLDAP($dn); - - $job = new CleanUp(); - $job->run([]); - - // user instance must not be requested from global user manager, before - // it is deleted from the LDAP server. The instance will be returned - // from cache and may false-positively confirm the correctness. - $user = \OC::$server->getUserManager()->get($username); - if ($user === null) { - return false; - } - $user->delete(); - - return null === \OC::$server->getUserManager()->get($username); - } -} - -/** @var string $host */ -/** @var int $port */ -/** @var string $adn */ -/** @var string $apwd */ -/** @var string $bdn */ -$test = new IntegrationTestUserCleanUp($host, $port, $adn, $apwd, $bdn); -$test->init(); -$test->run(); diff --git a/apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserDisplayName.php b/apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserDisplayName.php deleted file mode 100644 index a5712b164d1a3..0000000000000 --- a/apps/user_ldap/tests/Integration/Lib/User/IntegrationTestUserDisplayName.php +++ /dev/null @@ -1,113 +0,0 @@ - - * @author Joas Schilling - * @author Morris Jobke - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Integration\Lib\User; - -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\Tests\Integration\AbstractIntegrationTest; -use OCA\User_LDAP\User_LDAP; -use OCA\User_LDAP\UserPluginManager; - -require_once __DIR__ . '/../../Bootstrap.php'; - -class IntegrationTestUserDisplayName extends AbstractIntegrationTest { - /** @var UserMapping */ - protected $mapping; - - /** - * prepares the LDAP environment and sets up a test configuration for - * the LDAP backend. - */ - public function init() { - require(__DIR__ . '/../../setup-scripts/createExplicitUsers.php'); - parent::init(); - $this->mapping = new UserMapping(\OC::$server->getDatabaseConnection()); - $this->mapping->clear(); - $this->access->setUserMapper($this->mapping); - $userBackend = new User_LDAP($this->access, \OC::$server->getConfig(), \OC::$server->getNotificationManager(), \OC::$server->getUserSession(), \OC::$server->query(UserPluginManager::class)); - \OC_User::useBackend($userBackend); - } - - /** - * adds a map entry for the user, so we know the username - * - * @param $dn - * @param $username - */ - private function prepareUser($dn, $username) { - // assigns our self-picked oc username to the dn - $this->mapping->map($dn, $username, 'fakeUUID-' . $username); - } - - /** - * tests whether a display name consisting of two parts is created correctly - * - * @return bool - */ - protected function case1() { - $username = 'alice1337'; - $dn = 'uid=alice,ou=Users,' . $this->base; - $this->prepareUser($dn, $username); - $displayName = \OC::$server->getUserManager()->get($username)->getDisplayName(); - - return strpos($displayName, '(Alice@example.com)') !== false; - } - - /** - * tests whether a display name consisting of one part is created correctly - * - * @return bool - */ - protected function case2() { - $this->connection->setConfiguration([ - 'ldapUserDisplayName2' => '', - ]); - $username = 'boris23421'; - $dn = 'uid=boris,ou=Users,' . $this->base; - $this->prepareUser($dn, $username); - $displayName = \OC::$server->getUserManager()->get($username)->getDisplayName(); - - return strpos($displayName, '(Boris@example.com)') === false; - } - - /** - * sets up the LDAP configuration to be used for the test - */ - protected function initConnection() { - parent::initConnection(); - $this->connection->setConfiguration([ - 'ldapUserDisplayName' => 'displayName', - 'ldapUserDisplayName2' => 'mail', - ]); - } -} - -/** @var string $host */ -/** @var int $port */ -/** @var string $adn */ -/** @var string $apwd */ -/** @var string $bdn */ -$test = new IntegrationTestUserDisplayName($host, $port, $adn, $apwd, $bdn); -$test->init(); -$test->run(); diff --git a/apps/user_ldap/tests/Integration/data/avatar-invalid.gif b/apps/user_ldap/tests/Integration/data/avatar-invalid.gif deleted file mode 100644 index 000108834d8ef089a9f6a484e47c16eb4b5a8395..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48702 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 diff --git a/apps/user_ldap/tests/Integration/data/avatar-valid.jpg b/apps/user_ldap/tests/Integration/data/avatar-valid.jpg deleted file mode 100644 index 451b631eea6ddeb12eda82d8a75e33ed4ba60374..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75950 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

BH`5y1k9}?bn*+cw5p&AuJ=LO4xpMt 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 Z@7QBDX~GbN2t|m0!2LgSV;KH_|Jm-`D0=_^ diff --git a/apps/user_ldap/tests/Integration/readme.md b/apps/user_ldap/tests/Integration/readme.md deleted file mode 100644 index e20efef8fdc69..0000000000000 --- a/apps/user_ldap/tests/Integration/readme.md +++ /dev/null @@ -1,60 +0,0 @@ -# Requirements # - -Have (as in do copy if not already done) the following files from https://github.com/owncloud/administration/tree/master/ldap-testing copied into the directory "setup-scripts": - - * start.sh - * stop.sh - * config.php - -Configure config.php according to your needs, also have a look into the LDAP and network settings in start.sh and stop.sh. - -# Usage # - -The basic command to run a test is: - -```# ./run-test.sh [phpscript]``` - -Yes, run it as root from within this directory. - -Example: - -``` -$ sudo ./run-test.sh lib/IntegrationTestAccessGroupsMatchFilter.php -71cbe88a4993e67066714d71c1cecc5ef26a54911a208103cb6294f90459e574 -c74dc0155db4efa7a0515d419528a8727bbc7596601cf25b0df05e348bd74895 -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -c74dc0155db4 osixia/phpldapadmin:0.5.1 "/sbin/my_init" 1 seconds ago Up Less than a second 80/tcp, 0.0.0.0:8443->443/tcp docker-phpldapadmin -71cbe88a4993 nickstenning/slapd:latest "/sbin/my_init" 1 seconds ago Up Less than a second 127.0.0.1:7770->389/tcp docker-slapd - -LDAP server now available under 127.0.0.1:7770 (internal IP is 172.17.0.78) -phpldapadmin now available under https://127.0.0.1:8443 - -created user : Alice Ealic -created group : RedGroup -created group : BlueGroup -created group : GreenGroup -created group : PurpleGroup -running case1 -running case2 -Tests succeeded -Stopping and resetting containers -docker-slapd -docker-phpldapadmin -docker-slapd -docker-phpldapadmin -``` - -# How it works # - -1. start.sh is executed which brings up a fresh and clean OpenLDAP in Docker. -2. The provided test script is executed. It also outputs results. -3. stop.sh is executed to shut down OpenLDAP - -# Beware # - -This is quick solution for basically one test case. With expension this mechanism should be improved as well. - -It does not run automatically, unless you do it. No integration with any testing framework. - -exceptionOnLostConnection.php is not part of this mechanism. Read its source and run it isolated. While you're at it, port it :þ - diff --git a/apps/user_ldap/tests/Integration/run-all.sh b/apps/user_ldap/tests/Integration/run-all.sh deleted file mode 100755 index 02bab97e45fe1..0000000000000 --- a/apps/user_ldap/tests/Integration/run-all.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -trigger_notification() { - which notify-send 1>/dev/null - if [[ $? == 1 ]] ; then - return - fi - export NOTIFY_USER=$SUDO_USER - export RESULT_STR=$1 - # does not work. just pipe result into a non-sudo cmd - su "$NOTIFY_USER" -c "notify-send -u normal -t 43200000 -a Nextcloud -i Nextcloud \"LDAP Integration tests $RESULT_STR\"" -} - -FILES_ROOT=($(ls -d -p Lib/* | grep -v "/$")) -FILES_USER=($(ls -d -p Lib/User/* | grep -v "/$")) -# TODO: Loop through dirs (and subdirs?) once there are more -TESTFILES=("${FILES_ROOT[@]}" "${FILES_USER[@]}") - -TESTCMD="./run-test.sh" - -echo "Running " ${#TESTFILES[@]} " tests" -for TESTFILE in "${TESTFILES[@]}" ; do - echo -n "Test: $TESTFILE… " - STATE=`$TESTCMD "$TESTFILE" | grep -c "Tests succeeded"` - if [ "$STATE" -eq 0 ] ; then - echo "failed!" - trigger_notification "failed" - exit 1 - fi - echo "succeeded" -done - -echo -e "\nAll tests succeeded" -trigger_notification "succeeded" diff --git a/apps/user_ldap/tests/Integration/run-test.sh b/apps/user_ldap/tests/Integration/run-test.sh deleted file mode 100755 index 7a29db2567075..0000000000000 --- a/apps/user_ldap/tests/Integration/run-test.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -if [ $1 ] ; then - TESTSCRIPT=$1 -else - echo "No test file given" exit -fi - -if [ ! -e "$TESTSCRIPT" ] ; then - echo "Test file does not exist" - exit -fi - - -# sleep is necessary, otherwise the LDAP server cannot be connected to, yet. -setup-scripts/start.sh && sleep 5 && php -f "$TESTSCRIPT" -setup-scripts/stop.sh diff --git a/apps/user_ldap/tests/Integration/setup-scripts/createExplicitGroups.php b/apps/user_ldap/tests/Integration/setup-scripts/createExplicitGroups.php deleted file mode 100644 index 69bc90dedb245..0000000000000 --- a/apps/user_ldap/tests/Integration/setup-scripts/createExplicitGroups.php +++ /dev/null @@ -1,73 +0,0 @@ - - * @author Christoph Wurst - * @author Morris Jobke - * - * @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 - * - */ -if (php_sapi_name() !== 'cli') { - print('Only via CLI, please.'); - exit(1); -} - -include __DIR__ . '/config.php'; - -$cr = ldap_connect($host, $port); -ldap_set_option($cr, LDAP_OPT_PROTOCOL_VERSION, 3); -$ok = ldap_bind($cr, $adn, $apwd); - -if (!$ok) { - die(ldap_error($cr)); -} - -$ouName = 'Groups'; -$ouDN = 'ou=' . $ouName . ',' . $bdn; - -//creates an OU -if (true) { - $entry = []; - $entry['objectclass'][] = 'top'; - $entry['objectclass'][] = 'organizationalunit'; - $entry['ou'] = $ouName; - $b = ldap_add($cr, $ouDN, $entry); - if (!$b) { - die(ldap_error($cr)); - } -} - -$groups = ['RedGroup', 'BlueGroup', 'GreenGroup', 'PurpleGroup']; -// groupOfNames requires groups to have at least one member -// the member used is created by createExplicitUsers.php script -$omniMember = 'uid=alice,ou=Users,' . $bdn; - -foreach ($groups as $cn) { - $newDN = 'cn=' . $cn . ',' . $ouDN; - - $entry = []; - $entry['cn'] = $cn; - $entry['objectclass'][] = 'groupOfNames'; - $entry['member'][] = $omniMember; - - $ok = ldap_add($cr, $newDN, $entry); - if ($ok) { - echo('created group ' . ': ' . $entry['cn'] . PHP_EOL); - } else { - die(ldap_error($cr)); - } -} diff --git a/apps/user_ldap/tests/Integration/setup-scripts/createExplicitGroupsDifferentOU.php b/apps/user_ldap/tests/Integration/setup-scripts/createExplicitGroupsDifferentOU.php deleted file mode 100644 index e2607b0ccc39b..0000000000000 --- a/apps/user_ldap/tests/Integration/setup-scripts/createExplicitGroupsDifferentOU.php +++ /dev/null @@ -1,73 +0,0 @@ - - * @author Christoph Wurst - * @author Morris Jobke - * - * @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 - * - */ -if (php_sapi_name() !== 'cli') { - print('Only via CLI, please.'); - exit(1); -} - -include __DIR__ . '/config.php'; - -$cr = ldap_connect($host, $port); -ldap_set_option($cr, LDAP_OPT_PROTOCOL_VERSION, 3); -$ok = ldap_bind($cr, $adn, $apwd); - -if (!$ok) { - die(ldap_error($cr)); -} - -$ouName = 'SpecialGroups'; -$ouDN = 'ou=' . $ouName . ',' . $bdn; - -//creates an OU -if (true) { - $entry = []; - $entry['objectclass'][] = 'top'; - $entry['objectclass'][] = 'organizationalunit'; - $entry['ou'] = $ouName; - $b = ldap_add($cr, $ouDN, $entry); - if (!$b) { - die(ldap_error($cr)); - } -} - -$groups = ['SquareGroup', 'CircleGroup', 'TriangleGroup', 'SquaredCircleGroup']; -// groupOfNames requires groups to have at least one member -// the member used is created by createExplicitUsers.php script -$omniMember = 'uid=alice,ou=Users,' . $bdn; - -foreach ($groups as $cn) { - $newDN = 'cn=' . $cn . ',' . $ouDN; - - $entry = []; - $entry['cn'] = $cn; - $entry['objectclass'][] = 'groupOfNames'; - $entry['member'][] = $omniMember; - - $ok = ldap_add($cr, $newDN, $entry); - if ($ok) { - echo('created group ' . ': ' . $entry['cn'] . PHP_EOL); - } else { - die(ldap_error($cr)); - } -} diff --git a/apps/user_ldap/tests/Integration/setup-scripts/createExplicitUsers.php b/apps/user_ldap/tests/Integration/setup-scripts/createExplicitUsers.php deleted file mode 100644 index 6160179b9d46b..0000000000000 --- a/apps/user_ldap/tests/Integration/setup-scripts/createExplicitUsers.php +++ /dev/null @@ -1,76 +0,0 @@ - - * @author Christoph Wurst - * @author Morris Jobke - * - * @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 - * - */ -if (php_sapi_name() !== 'cli') { - print('Only via CLI, please.'); - exit(1); -} - -include __DIR__ . '/config.php'; - -$cr = ldap_connect($host, $port); -ldap_set_option($cr, LDAP_OPT_PROTOCOL_VERSION, 3); -$ok = ldap_bind($cr, $adn, $apwd); - -if (!$ok) { - die(ldap_error($cr)); -} - -$ouName = 'Users'; -$ouDN = 'ou=' . $ouName . ',' . $bdn; - -//creates on OU -if (true) { - $entry = []; - $entry['objectclass'][] = 'top'; - $entry['objectclass'][] = 'organizationalunit'; - $entry['ou'] = $ouName; - $b = ldap_add($cr, $ouDN, $entry); - if (!$b) { - die(ldap_error($cr)); - } -} - -$users = ['alice', 'boris', 'cynthia', 'derek', 'evelina', 'fatima', 'gregor']; - -foreach ($users as $uid) { - $newDN = 'uid=' . $uid . ',' . $ouDN; - $fn = ucfirst($uid); - $sn = ucfirst(str_shuffle($uid)); // not so explicit but it's OK. - - $entry = []; - $entry['cn'] = $fn . ' ' . $sn; - $entry['objectclass'][] = 'inetOrgPerson'; - $entry['objectclass'][] = 'person'; - $entry['sn'] = $sn; - $entry['userPassword'] = $uid; - $entry['displayName'] = $sn . ', ' . $fn; - $entry['mail'] = $fn . '@example.com'; - - $ok = ldap_add($cr, $newDN, $entry); - if ($ok) { - echo('created user ' . ': ' . $entry['cn'] . PHP_EOL); - } else { - die(ldap_error($cr)); - } -} diff --git a/apps/user_ldap/tests/Integration/setup-scripts/createUsersWithoutDisplayName.php b/apps/user_ldap/tests/Integration/setup-scripts/createUsersWithoutDisplayName.php deleted file mode 100644 index 5a36ce24838db..0000000000000 --- a/apps/user_ldap/tests/Integration/setup-scripts/createUsersWithoutDisplayName.php +++ /dev/null @@ -1,61 +0,0 @@ - - * @author Christoph Wurst - * - * @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 - * - */ -if (php_sapi_name() !== 'cli') { - print('Only via CLI, please.'); - exit(1); -} - -include __DIR__ . '/config.php'; - -$cr = ldap_connect($host, $port); -ldap_set_option($cr, LDAP_OPT_PROTOCOL_VERSION, 3); -$ok = ldap_bind($cr, $adn, $apwd); - -if (!$ok) { - die(ldap_error($cr)); -} - -$ouName = 'Users'; -$ouDN = 'ou=' . $ouName . ',' . $bdn; - -$users = ['robot']; - -foreach ($users as $uid) { - $newDN = 'uid=' . $uid . ',' . $ouDN; - $fn = ucfirst($uid); - $sn = ucfirst(str_shuffle($uid)); // not so explicit but it's OK. - - $entry = []; - $entry['cn'] = ucfirst($uid); - $entry['objectclass'][] = 'inetOrgPerson'; - $entry['objectclass'][] = 'person'; - $entry['sn'] = $sn; - $entry['userPassword'] = $uid; - - $ok = ldap_add($cr, $newDN, $entry); - if ($ok) { - echo('created user ' . ': ' . $entry['cn'] . PHP_EOL); - } else { - die(ldap_error($cr)); - } -} diff --git a/apps/user_ldap/tests/Jobs/CleanUpTest.php b/apps/user_ldap/tests/Jobs/CleanUpTest.php deleted file mode 100644 index 73d246ac4e215..0000000000000 --- a/apps/user_ldap/tests/Jobs/CleanUpTest.php +++ /dev/null @@ -1,155 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Jobs; - -use OCA\User_LDAP\Helper; -use OCP\IConfig; -use OCP\IDBConnection; - -class CleanUpTest extends \Test\TestCase { - public function getMocks() { - $mocks = []; - $mocks['userBackend'] = - $this->getMockBuilder('\OCA\User_LDAP\User_Proxy') - ->disableOriginalConstructor() - ->getMock(); - $mocks['deletedUsersIndex'] = - $this->getMockBuilder('\OCA\User_LDAP\User\DeletedUsersIndex') - ->disableOriginalConstructor() - ->getMock(); - $mocks['ocConfig'] = $this->createMock(IConfig::class); - $mocks['db'] = $this->createMock(IDBConnection::class); - $mocks['helper'] = $this->createMock(Helper::class); - - return $mocks; - } - - /** - * clean up job must not run when there are disabled configurations - */ - public function test_runNotAllowedByDisabledConfigurations() { - $args = $this->getMocks(); - $args['helper']->expects($this->once()) - ->method('haveDisabledConfigurations') - ->willReturn(true); - - $args['ocConfig']->expects($this->never()) - ->method('getSystemValue'); - - $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); - $bgJob->setArguments($args); - - $result = $bgJob->isCleanUpAllowed(); - $this->assertSame(false, $result); - } - - /** - * clean up job must not run when LDAP Helper is broken i.e. - * returning unexpected results - */ - public function test_runNotAllowedByBrokenHelper() { - $args = $this->getMocks(); - $args['helper']->expects($this->once()) - ->method('haveDisabledConfigurations') - ->will($this->throwException(new \Exception())); - - $args['ocConfig']->expects($this->never()) - ->method('getSystemValue'); - - $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); - $bgJob->setArguments($args); - - $result = $bgJob->isCleanUpAllowed(); - $this->assertSame(false, $result); - } - - /** - * clean up job must not run when it is not enabled - */ - public function test_runNotAllowedBySysConfig() { - $args = $this->getMocks(); - $args['helper']->expects($this->once()) - ->method('haveDisabledConfigurations') - ->willReturn(false); - - $args['ocConfig']->expects($this->once()) - ->method('getSystemValue') - ->willReturn(false); - - $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); - $bgJob->setArguments($args); - - $result = $bgJob->isCleanUpAllowed(); - $this->assertSame(false, $result); - } - - /** - * clean up job is allowed to run - */ - public function test_runIsAllowed() { - $args = $this->getMocks(); - $args['helper']->expects($this->once()) - ->method('haveDisabledConfigurations') - ->willReturn(false); - - $args['ocConfig']->expects($this->once()) - ->method('getSystemValue') - ->willReturn(true); - - $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); - $bgJob->setArguments($args); - - $result = $bgJob->isCleanUpAllowed(); - $this->assertSame(true, $result); - } - - /** - * check whether offset will be reset when it needs to - */ - public function test_OffsetResetIsNecessary() { - $args = $this->getMocks(); - - $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); - $bgJob->setArguments($args); - - $result = $bgJob->isOffsetResetNecessary($bgJob->getChunkSize() - 1); - $this->assertSame(true, $result); - } - - /** - * make sure offset is not reset when it is not due - */ - public function test_OffsetResetIsNotNecessary() { - $args = $this->getMocks(); - - $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); - $bgJob->setArguments($args); - - $result = $bgJob->isOffsetResetNecessary($bgJob->getChunkSize()); - $this->assertSame(false, $result); - } -} diff --git a/apps/user_ldap/tests/Jobs/SyncTest.php b/apps/user_ldap/tests/Jobs/SyncTest.php deleted file mode 100644 index 71868dcc6172e..0000000000000 --- a/apps/user_ldap/tests/Jobs/SyncTest.php +++ /dev/null @@ -1,386 +0,0 @@ - - * - * @author Arthur Schiwon - * @author Christoph Wurst - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests\Jobs; - -use OCA\User_LDAP\Access; -use OCA\User_LDAP\AccessFactory; -use OCA\User_LDAP\Connection; -use OCA\User_LDAP\ConnectionFactory; -use OCA\User_LDAP\Helper; -use OCA\User_LDAP\Jobs\Sync; -use OCA\User_LDAP\LDAP; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\User\Manager; -use OCP\IAvatarManager; -use OCP\IConfig; -use OCP\IDBConnection; -use OCP\IUserManager; -use OCP\Notification\IManager; -use Test\TestCase; - -class SyncTest extends TestCase { - - /** @var array */ - protected $arguments; - /** @var Helper|\PHPUnit\Framework\MockObject\MockObject */ - protected $helper; - /** @var LDAP|\PHPUnit\Framework\MockObject\MockObject */ - protected $ldapWrapper; - /** @var Manager|\PHPUnit\Framework\MockObject\MockObject */ - protected $userManager; - /** @var UserMapping|\PHPUnit\Framework\MockObject\MockObject */ - protected $mapper; - /** @var Sync */ - protected $sync; - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - protected $config; - /** @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $avatarManager; - /** @var IDBConnection|\PHPUnit\Framework\MockObject\MockObject */ - protected $dbc; - /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $ncUserManager; - /** @var IManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $notificationManager; - /** @var ConnectionFactory|\PHPUnit\Framework\MockObject\MockObject */ - protected $connectionFactory; - /** @var AccessFactory|\PHPUnit\Framework\MockObject\MockObject */ - protected $accessFactory; - - protected function setUp(): void { - parent::setUp(); - - $this->helper = $this->createMock(Helper::class); - $this->ldapWrapper = $this->createMock(LDAP::class); - $this->userManager = $this->createMock(Manager::class); - $this->mapper = $this->createMock(UserMapping::class); - $this->config = $this->createMock(IConfig::class); - $this->avatarManager = $this->createMock(IAvatarManager::class); - $this->dbc = $this->createMock(IDBConnection::class); - $this->ncUserManager = $this->createMock(IUserManager::class); - $this->notificationManager = $this->createMock(IManager::class); - $this->connectionFactory = $this->createMock(ConnectionFactory::class); - $this->accessFactory = $this->createMock(AccessFactory::class); - - $this->arguments = [ - 'helper' => $this->helper, - 'ldapWrapper' => $this->ldapWrapper, - 'userManager' => $this->userManager, - 'mapper' => $this->mapper, - 'config' => $this->config, - 'avatarManager' => $this->avatarManager, - 'dbc' => $this->dbc, - 'ncUserManager' => $this->ncUserManager, - 'notificationManager' => $this->notificationManager, - 'connectionFactory' => $this->connectionFactory, - 'accessFactory' => $this->accessFactory, - ]; - - $this->sync = new Sync(); - } - - public function intervalDataProvider() { - return [ - [ - 0, 1000, 750 - ], - [ - 22, 0, 50 - ], - [ - 500, 500, 500 - ], - [ - 1357, 0, 0 - ], - [ - 421337, 2000, 3000 - ] - ]; - } - - /** - * @dataProvider intervalDataProvider - */ - public function testUpdateInterval($userCount, $pagingSize1, $pagingSize2) { - $this->config->expects($this->once()) - ->method('setAppValue') - ->with('user_ldap', 'background_sync_interval', $this->anything()) - ->willReturnCallback(function ($a, $k, $interval) { - $this->assertTrue($interval >= SYNC::MIN_INTERVAL); - $this->assertTrue($interval <= SYNC::MAX_INTERVAL); - return true; - }); - $this->config->expects($this->atLeastOnce()) - ->method('getAppKeys') - ->willReturn([ - 'blabla', - 'ldap_paging_size', - 's07blabla', - 'installed', - 's07ldap_paging_size' - ]); - $this->config->expects($this->exactly(2)) - ->method('getAppValue') - ->willReturnOnConsecutiveCalls($pagingSize1, $pagingSize2); - - $this->mapper->expects($this->atLeastOnce()) - ->method('count') - ->willReturn($userCount); - - $this->sync->setArgument($this->arguments); - $this->sync->updateInterval(); - } - - public function moreResultsProvider() { - return [ - [ 3, 3, true ], - [ 3, 5, true ], - [ 3, 2, false], - [ 0, 4, false], - [ null, 4, false] - ]; - } - - /** - * @dataProvider moreResultsProvider - */ - public function testMoreResults($pagingSize, $results, $expected) { - $connection = $this->createMock(Connection::class); - $this->connectionFactory->expects($this->any()) - ->method('get') - ->willReturn($connection); - $connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($key) use ($pagingSize) { - if ($key === 'ldapPagingSize') { - return $pagingSize; - } - return null; - }); - - /** @var Access|\PHPUnit\Framework\MockObject\MockObject $access */ - $access = $this->createMock(Access::class); - $this->accessFactory->expects($this->any()) - ->method('get') - ->with($connection) - ->willReturn($access); - - $this->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['dn', 'uid', 'mail', 'displayname']); - - $access->expects($this->once()) - ->method('fetchListOfUsers') - ->willReturn(array_pad([], $results, 'someUser')); - $access->expects($this->any()) - ->method('combineFilterWithAnd') - ->willReturn('pseudo=filter'); - $access->connection = $connection; - $access->userManager = $this->userManager; - - $this->sync->setArgument($this->arguments); - $hasMoreResults = $this->sync->runCycle(['prefix' => 's01', 'offset' => 100]); - $this->assertSame($expected, $hasMoreResults); - } - - public function cycleDataProvider() { - $lastCycle = ['prefix' => 's01', 'offset' => 1000]; - $lastCycle2 = ['prefix' => '', 'offset' => 1000]; - return [ - [ null, ['s01'], ['prefix' => 's01', 'offset' => 0] ], - [ null, [''], ['prefix' => '', 'offset' => 0] ], - [ $lastCycle, ['s01', 's02'], ['prefix' => 's02', 'offset' => 0] ], - [ $lastCycle, [''], ['prefix' => '', 'offset' => 0] ], - [ $lastCycle2, ['', 's01'], ['prefix' => 's01', 'offset' => 0] ], - [ $lastCycle, [], null ], - ]; - } - - /** - * @dataProvider cycleDataProvider - */ - public function testDetermineNextCycle($cycleData, $prefixes, $expectedCycle) { - $this->helper->expects($this->any()) - ->method('getServerConfigurationPrefixes') - ->with(true) - ->willReturn($prefixes); - - if (is_array($expectedCycle)) { - $this->config->expects($this->exactly(2)) - ->method('setAppValue') - ->withConsecutive( - ['user_ldap', 'background_sync_prefix', $expectedCycle['prefix']], - ['user_ldap', 'background_sync_offset', $expectedCycle['offset']] - ); - } else { - $this->config->expects($this->never()) - ->method('setAppValue'); - } - - $this->sync->setArgument($this->arguments); - $nextCycle = $this->sync->determineNextCycle($cycleData); - - if ($expectedCycle === null) { - $this->assertNull($nextCycle); - } else { - $this->assertSame($expectedCycle['prefix'], $nextCycle['prefix']); - $this->assertSame($expectedCycle['offset'], $nextCycle['offset']); - } - } - - public function testQualifiesToRun() { - $cycleData = ['prefix' => 's01']; - - $this->config->expects($this->exactly(2)) - ->method('getAppValue') - ->willReturnOnConsecutiveCalls(time() - 60*40, time() - 60*20); - - $this->sync->setArgument($this->arguments); - $this->assertTrue($this->sync->qualifiesToRun($cycleData)); - $this->assertFalse($this->sync->qualifiesToRun($cycleData)); - } - - public function runDataProvider() { - return [ - #0 - one LDAP server, reset - [[ - 'prefixes' => [''], - 'scheduledCycle' => ['prefix' => '', 'offset' => '4500'], - 'pagingSize' => 500, - 'usersThisCycle' => 0, - 'expectedNextCycle' => ['prefix' => '', 'offset' => '0'], - 'mappedUsers' => 123, - ]], - #1 - 2 LDAP servers, next prefix - [[ - 'prefixes' => ['', 's01'], - 'scheduledCycle' => ['prefix' => '', 'offset' => '4500'], - 'pagingSize' => 500, - 'usersThisCycle' => 0, - 'expectedNextCycle' => ['prefix' => 's01', 'offset' => '0'], - 'mappedUsers' => 123, - ]], - #2 - 2 LDAP servers, rotate prefix - [[ - 'prefixes' => ['', 's01'], - 'scheduledCycle' => ['prefix' => 's01', 'offset' => '4500'], - 'pagingSize' => 500, - 'usersThisCycle' => 0, - 'expectedNextCycle' => ['prefix' => '', 'offset' => '0'], - 'mappedUsers' => 123, - ]], - ]; - } - - /** - * @dataProvider runDataProvider - */ - public function testRun($runData) { - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback(function ($app, $key, $default) use ($runData) { - if ($app === 'core' && $key === 'backgroundjobs_mode') { - return 'cron'; - } - if ($app = 'user_ldap') { - // for getCycle() - if ($key === 'background_sync_prefix') { - return $runData['scheduledCycle']['prefix']; - } - if ($key === 'background_sync_offset') { - return $runData['scheduledCycle']['offset']; - } - // for qualifiesToRun() - if ($key === $runData['scheduledCycle']['prefix'] . '_lastChange') { - return time() - 60*40; - } - // for getMinPagingSize - if ($key === $runData['scheduledCycle']['prefix'] . 'ldap_paging_size') { - return $runData['pagingSize']; - } - } - - return $default; - }); - $this->config->expects($this->exactly(3)) - ->method('setAppValue') - ->withConsecutive( - ['user_ldap', 'background_sync_prefix', $runData['expectedNextCycle']['prefix']], - ['user_ldap', 'background_sync_offset', $runData['expectedNextCycle']['offset']], - ['user_ldap', 'background_sync_interval', $this->anything()] - ); - $this->config->expects($this->any()) - ->method('getAppKeys') - ->with('user_ldap') - ->willReturn([$runData['scheduledCycle']['prefix'] . 'ldap_paging_size']); - - $this->helper->expects($this->any()) - ->method('getServerConfigurationPrefixes') - ->with(true) - ->willReturn($runData['prefixes']); - - $connection = $this->createMock(Connection::class); - $this->connectionFactory->expects($this->any()) - ->method('get') - ->willReturn($connection); - $connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($key) use ($runData) { - if ($key === 'ldapPagingSize') { - return $runData['pagingSize']; - } - return null; - }); - - /** @var Access|\PHPUnit\Framework\MockObject\MockObject $access */ - $access = $this->createMock(Access::class); - $this->accessFactory->expects($this->any()) - ->method('get') - ->with($connection) - ->willReturn($access); - - $this->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['dn', 'uid', 'mail', 'displayname']); - - $access->expects($this->once()) - ->method('fetchListOfUsers') - ->willReturn(array_pad([], $runData['usersThisCycle'], 'someUser')); - $access->expects($this->any()) - ->method('combineFilterWithAnd') - ->willReturn('pseudo=filter'); - $access->connection = $connection; - $access->userManager = $this->userManager; - - $this->mapper->expects($this->any()) - ->method('count') - ->willReturn($runData['mappedUsers']); - - $this->sync->run($this->arguments); - } -} diff --git a/apps/user_ldap/tests/LDAPGroupPluginDummy.php b/apps/user_ldap/tests/LDAPGroupPluginDummy.php deleted file mode 100644 index 7ff5cd8ac69c6..0000000000000 --- a/apps/user_ldap/tests/LDAPGroupPluginDummy.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\ILDAPGroupPlugin; - -class LDAPGroupPluginDummy implements ILDAPGroupPlugin { - public function respondToActions() { - return null; - } - - public function createGroup($gid) { - return null; - } - - public function deleteGroup($gid) { - return null; - } - - public function addToGroup($uid, $gid) { - return null; - } - - public function removeFromGroup($uid, $gid) { - return null; - } - - public function countUsersInGroup($gid, $search = '') { - return null; - } - - public function getGroupDetails($gid) { - return null; - } -} diff --git a/apps/user_ldap/tests/LDAPProviderTest.php b/apps/user_ldap/tests/LDAPProviderTest.php deleted file mode 100644 index a8910c4a272b6..0000000000000 --- a/apps/user_ldap/tests/LDAPProviderTest.php +++ /dev/null @@ -1,699 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Julius Härtl - * @author Roeland Jago Douma - * @author root - * @author Vinicius Cubas Brand - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests; - -use OC\User\Manager; -use OCA\User_LDAP\Access; -use OCA\User_LDAP\Connection; -use OCA\User_LDAP\IGroupLDAP; -use OCA\User_LDAP\IUserLDAP; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\IConfig; -use OCP\IServerContainer; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; - -/** - * Class LDAPProviderTest - * - * @group DB - * - * @package OCA\User_LDAP\Tests - */ -class LDAPProviderTest extends \Test\TestCase { - protected function setUp(): void { - parent::setUp(); - } - - private function getServerMock(IUserLDAP $userBackend, IGroupLDAP $groupBackend) { - $server = $this->getMockBuilder('OC\Server') - ->setMethods(['getUserManager', 'getBackends', 'getGroupManager']) - ->setConstructorArgs(['', new \OC\Config(\OC::$configDir)]) - ->getMock(); - $server->expects($this->at(1)) - ->method('getBackends') - ->willReturn([$userBackend]); - $server->expects($this->any()) - ->method('getUserManager') - ->willReturn($this->getUserManagerMock($userBackend)); - $server->expects($this->any()) - ->method('getGroupManager') - ->willReturn($this->getGroupManagerMock($groupBackend)); - $server->expects($this->any()) - ->method($this->anything()) - ->willReturnSelf(); - - return $server; - } - - private function getUserManagerMock(IUserLDAP $userBackend) { - $userManager = $this->getMockBuilder(Manager::class) - ->setMethods(['getBackends']) - ->setConstructorArgs([ - $this->createMock(IConfig::class), - $this->createMock(EventDispatcherInterface::class), - $this->createMock(IEventDispatcher::class) - ]) - ->getMock(); - $userManager->expects($this->any()) - ->method('getBackends') - ->willReturn([$userBackend]); - return $userManager; - } - - private function getGroupManagerMock(IGroupLDAP $groupBackend) { - $groupManager = $this->getMockBuilder('OC\Group\Manager') - ->setMethods(['getBackends']) - ->disableOriginalConstructor() - ->getMock(); - $groupManager->expects($this->any()) - ->method('getBackends') - ->willReturn([$groupBackend]); - return $groupManager; - } - - private function getDefaultGroupBackendMock() { - $groupBackend = $this->getMockBuilder('OCA\User_LDAP\Group_LDAP') - ->disableOriginalConstructor() - ->getMock(); - - return $groupBackend; - } - - private function getLDAPProvider(IServerContainer $serverContainer) { - $factory = new \OCA\User_LDAP\LDAPProviderFactory($serverContainer); - return $factory->getLDAPProvider(); - } - - - public function testGetUserDNUserIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('User id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any())->method('userExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->getUserDN('nonexisting_user'); - } - - public function testGetUserDN() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists', 'getLDAPAccess', 'username2dn']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->at(0)) - ->method('userExists') - ->willReturn(true); - $userBackend->expects($this->at(2)) - ->method('username2dn') - ->willReturn('cn=existing_user,ou=Are Sufficient To,ou=Test,dc=example,dc=org'); - $userBackend->expects($this->any()) - ->method($this->anything()) - ->willReturnSelf(); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertEquals('cn=existing_user,ou=Are Sufficient To,ou=Test,dc=example,dc=org', - $ldapProvider->getUserDN('existing_user')); - } - - - public function testGetGroupDNGroupIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Group id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend = $this->getMockBuilder('OCA\User_LDAP\Group_LDAP') - ->setMethods(['groupExists']) - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend->expects($this->any())->method('groupExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $groupBackend); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->getGroupDN('nonexisting_group'); - } - - public function testGetGroupDN() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists', 'getLDAPAccess', 'username2dn']) - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend = $this->getMockBuilder('OCA\User_LDAP\Group_LDAP') - ->setMethods(['groupExists', 'getLDAPAccess', 'groupname2dn']) - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend->expects($this->at(0)) - ->method('groupExists') - ->willReturn(true); - $groupBackend->expects($this->at(2)) - ->method('groupname2dn') - ->willReturn('cn=existing_group,ou=Are Sufficient To,ou=Test,dc=example,dc=org'); - $groupBackend->expects($this->any()) - ->method($this->anything()) - ->willReturnSelf(); - - $server = $this->getServerMock($userBackend, $groupBackend); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertEquals('cn=existing_group,ou=Are Sufficient To,ou=Test,dc=example,dc=org', - $ldapProvider->getGroupDN('existing_group')); - } - - public function testGetUserName() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['dn2UserName']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any()) - ->method('dn2UserName') - ->willReturn('existing_user'); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertEquals('existing_user', - $ldapProvider->getUserName('cn=existing_user,ou=Are Sufficient To,ou=Test,dc=example,dc=org')); - } - - public function testDNasBaseParameter() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods([]) - ->disableOriginalConstructor() - ->getMock(); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $helper = new \OCA\User_LDAP\Helper(\OC::$server->getConfig()); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertEquals( - $helper->DNasBaseParameter('cn=existing_user,ou=Are Sufficient To,ou=Test,dc=example,dc=org'), - $ldapProvider->DNasBaseParameter('cn=existing_user,ou=Are Sufficient To,ou=Test,dc=example,dc=org')); - } - - public function testSanitizeDN() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods([]) - ->disableOriginalConstructor() - ->getMock(); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $helper = new \OCA\User_LDAP\Helper(\OC::$server->getConfig()); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertEquals( - $helper->sanitizeDN('cn=existing_user,ou=Are Sufficient To,ou=Test,dc=example,dc=org'), - $ldapProvider->sanitizeDN('cn=existing_user,ou=Are Sufficient To,ou=Test,dc=example,dc=org')); - } - - - public function testGetLDAPConnectionUserIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('User id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any())->method('userExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->getLDAPConnection('nonexisting_user'); - } - - public function testGetLDAPConnection() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists', 'getNewLDAPConnection']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any()) - ->method('userExists') - ->willReturn(true); - $userBackend->expects($this->any()) - ->method('getNewLDAPConnection') - ->willReturn(true); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertTrue($ldapProvider->getLDAPConnection('existing_user')); - } - - - public function testGetGroupLDAPConnectionGroupIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Group id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend = $this->getMockBuilder('OCA\User_LDAP\Group_LDAP') - ->setMethods(['groupExists']) - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend->expects($this->any())->method('groupExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $groupBackend); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->getGroupLDAPConnection('nonexisting_group'); - } - - public function testGetGroupLDAPConnection() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend = $this->getMockBuilder('OCA\User_LDAP\Group_LDAP') - ->setMethods(['groupExists','getNewLDAPConnection']) - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend->expects($this->any()) - ->method('groupExists') - ->willReturn(true); - - $groupBackend->expects($this->any()) - ->method('getNewLDAPConnection') - ->willReturn(true); - - $server = $this->getServerMock($userBackend, $groupBackend); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertTrue($ldapProvider->getGroupLDAPConnection('existing_group')); - } - - - public function testGetLDAPBaseUsersUserIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('User id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any())->method('userExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->getLDAPBaseUsers('nonexisting_user'); - } - - public function testGetLDAPBaseUsers() { - $bases = [ - 'ou=users,ou=foobar,dc=example,dc=org', - 'ou=users,ou=barfoo,dc=example,dc=org', - ]; - $dn = 'uid=malik,' . $bases[1]; - - $connection = $this->createMock(Connection::class); - $connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($key) use ($bases) { - switch ($key) { - case 'ldapBaseUsers': - return $bases; - } - return null; - }); - - $access = $this->createMock(Access::class); - $access->expects($this->any()) - ->method('getConnection') - ->willReturn($connection); - $access->expects($this->exactly(2)) - ->method('isDNPartOfBase') - ->willReturnOnConsecutiveCalls(false, true); - $access->expects($this->atLeastOnce()) - ->method('username2dn') - ->willReturn($dn); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists', 'getLDAPAccess', 'getConnection', 'getConfiguration']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->atLeastOnce()) - ->method('userExists') - ->willReturn(true); - $userBackend->expects($this->any()) - ->method('getLDAPAccess') - ->willReturn($access); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertEquals($bases[1], $ldapProvider->getLDAPBaseUsers('existing_user')); - } - - - public function testGetLDAPBaseGroupsUserIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('User id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any())->method('userExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->getLDAPBaseGroups('nonexisting_user'); - } - - public function testGetLDAPBaseGroups() { - $bases = [ - 'ou=groupd,ou=foobar,dc=example,dc=org', - 'ou=groups,ou=barfoo,dc=example,dc=org', - ]; - - $connection = $this->createMock(Connection::class); - $connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($key) use ($bases) { - switch ($key) { - case 'ldapBaseGroups': - return $bases; - } - return null; - }); - - $access = $this->createMock(Access::class); - $access->expects($this->any()) - ->method('getConnection') - ->willReturn($connection); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists', 'getLDAPAccess', 'getConnection', 'getConfiguration']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any()) - ->method('userExists') - ->willReturn(true); - $userBackend->expects($this->any()) - ->method('getLDAPAccess') - ->willReturn($access); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertEquals($bases[0], $ldapProvider->getLDAPBaseGroups('existing_user')); - } - - - public function testClearCacheUserIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('User id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any())->method('userExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->clearCache('nonexisting_user'); - } - - public function testClearCache() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists', 'getLDAPAccess', 'getConnection', 'clearCache']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->at(0)) - ->method('userExists') - ->willReturn(true); - $userBackend->expects($this->at(3)) - ->method('clearCache') - ->willReturn(true); - $userBackend->expects($this->any()) - ->method($this->anything()) - ->willReturnSelf(); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->clearCache('existing_user'); - $this->addToAssertionCount(1); - } - - - public function testClearGroupCacheGroupIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Group id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->disableOriginalConstructor() - ->getMock(); - $groupBackend = $this->getMockBuilder('OCA\User_LDAP\Group_LDAP') - ->setMethods(['groupExists']) - ->disableOriginalConstructor() - ->getMock(); - $groupBackend->expects($this->any())->method('groupExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $groupBackend); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->clearGroupCache('nonexisting_group'); - } - - public function testClearGroupCache() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->disableOriginalConstructor() - ->getMock(); - $groupBackend = $this->getMockBuilder('OCA\User_LDAP\Group_LDAP') - ->setMethods(['groupExists', 'getLDAPAccess', 'getConnection', 'clearCache']) - ->disableOriginalConstructor() - ->getMock(); - $groupBackend->expects($this->at(0)) - ->method('groupExists') - ->willReturn(true); - $groupBackend->expects($this->at(3)) - ->method('clearCache') - ->willReturn(true); - $groupBackend->expects($this->any()) - ->method($this->anything()) - ->willReturnSelf(); - - $server = $this->getServerMock($userBackend, $groupBackend); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->clearGroupCache('existing_group'); - $this->addToAssertionCount(1); - } - - public function testDnExists() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['dn2UserName']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any()) - ->method('dn2UserName') - ->willReturn('existing_user'); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertTrue($ldapProvider->dnExists('cn=existing_user,ou=Are Sufficient To,ou=Test,dc=example,dc=org')); - } - - public function testFlagRecord() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods([]) - ->disableOriginalConstructor() - ->getMock(); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->flagRecord('existing_user'); - $this->addToAssertionCount(1); - } - - public function testUnflagRecord() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods([]) - ->disableOriginalConstructor() - ->getMock(); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->unflagRecord('existing_user'); - $this->addToAssertionCount(1); - } - - - public function testGetLDAPDisplayNameFieldUserIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('User id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any())->method('userExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->getLDAPDisplayNameField('nonexisting_user'); - } - - public function testGetLDAPDisplayNameField() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists', 'getLDAPAccess', 'getConnection', 'getConfiguration']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->at(0)) - ->method('userExists') - ->willReturn(true); - $userBackend->expects($this->at(3)) - ->method('getConfiguration') - ->willReturn(['ldap_display_name'=>'displayName']); - $userBackend->expects($this->any()) - ->method($this->anything()) - ->willReturnSelf(); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertEquals('displayName', $ldapProvider->getLDAPDisplayNameField('existing_user')); - } - - - public function testGetLDAPEmailFieldUserIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('User id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->any())->method('userExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->getLDAPEmailField('nonexisting_user'); - } - - public function testGetLDAPEmailField() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->setMethods(['userExists', 'getLDAPAccess', 'getConnection', 'getConfiguration']) - ->disableOriginalConstructor() - ->getMock(); - $userBackend->expects($this->at(0)) - ->method('userExists') - ->willReturn(true); - $userBackend->expects($this->at(3)) - ->method('getConfiguration') - ->willReturn(['ldap_email_attr'=>'mail']); - $userBackend->expects($this->any()) - ->method($this->anything()) - ->willReturnSelf(); - - $server = $this->getServerMock($userBackend, $this->getDefaultGroupBackendMock()); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertEquals('mail', $ldapProvider->getLDAPEmailField('existing_user')); - } - - - public function testGetLDAPGroupMemberAssocUserIDNotFound() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Group id not found in LDAP'); - - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend = $this->getMockBuilder('OCA\User_LDAP\Group_LDAP') - ->setMethods(['groupExists']) - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend->expects($this->any())->method('groupExists')->willReturn(false); - - $server = $this->getServerMock($userBackend, $groupBackend); - - $ldapProvider = $this->getLDAPProvider($server); - $ldapProvider->getLDAPGroupMemberAssoc('nonexisting_group'); - } - - public function testgetLDAPGroupMemberAssoc() { - $userBackend = $this->getMockBuilder('OCA\User_LDAP\User_LDAP') - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend = $this->getMockBuilder('OCA\User_LDAP\Group_LDAP') - ->setMethods(['groupExists', 'getLDAPAccess', 'getConnection', 'getConfiguration']) - ->disableOriginalConstructor() - ->getMock(); - - $groupBackend->expects($this->at(0)) - ->method('groupExists') - ->willReturn(true); - $groupBackend->expects($this->any()) - ->method('getConfiguration') - ->willReturn(['ldap_group_member_assoc_attribute'=>'assoc_type']); - $groupBackend->expects($this->any()) - ->method($this->anything()) - ->willReturnSelf(); - - $server = $this->getServerMock($userBackend, $groupBackend); - - $ldapProvider = $this->getLDAPProvider($server); - $this->assertEquals('assoc_type', $ldapProvider->getLDAPGroupMemberAssoc('existing_group')); - } -} diff --git a/apps/user_ldap/tests/LDAPTest.php b/apps/user_ldap/tests/LDAPTest.php deleted file mode 100644 index 443bafe505b43..0000000000000 --- a/apps/user_ldap/tests/LDAPTest.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * @author Arthur Schiwon - * @author Christoph Wurst - * @author Lukas Reschke - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\LDAP; -use Test\TestCase; - -class LDAPTest extends TestCase { - /** @var LDAP|\PHPUnit\Framework\MockObject\MockObject */ - private $ldap; - - protected function setUp(): void { - parent::setUp(); - $this->ldap = $this->getMockBuilder(LDAP::class) - ->setMethods(['invokeLDAPMethod']) - ->getMock(); - } - - public function errorProvider() { - return [ - [ - 'ldap_search(): Partial search results returned: Sizelimit exceeded at /srv/http/nextcloud/master/apps/user_ldap/lib/LDAP.php#292', - false - ], - [ - 'Some other error', true - ] - ]; - } - - /** - * @param string $errorMessage - * @param bool $passThrough - * @dataProvider errorProvider - */ - public function testSearchWithErrorHandler(string $errorMessage, bool $passThrough) { - $wasErrorHandlerCalled = false; - $errorHandler = function ($number, $message, $file, $line) use (&$wasErrorHandlerCalled) { - $wasErrorHandlerCalled = true; - }; - - set_error_handler($errorHandler); - - $this->ldap - ->expects($this->once()) - ->method('invokeLDAPMethod') - ->with('search', $this->anything(), $this->anything(), $this->anything(), $this->anything(), $this->anything()) - ->willReturnCallback(function () use ($errorMessage) { - trigger_error($errorMessage); - }); - - $fakeResource = ldap_connect(); - $this->ldap->search($fakeResource, 'base', 'filter', []); - $this->assertSame($wasErrorHandlerCalled, $passThrough); - - restore_error_handler(); - } - - public function testModReplace() { - $link = $this->createMock(LDAP::class); - $userDN = 'CN=user'; - $password = 'MyPassword'; - $this->ldap - ->expects($this->once()) - ->method('invokeLDAPMethod') - ->with('mod_replace', $link, $userDN, ['userPassword' => $password]) - ->willReturn(true); - - $this->assertTrue($this->ldap->modReplace($link, $userDN, $password)); - } -} diff --git a/apps/user_ldap/tests/LDAPUserPluginDummy.php b/apps/user_ldap/tests/LDAPUserPluginDummy.php deleted file mode 100644 index a3bcc252fbeae..0000000000000 --- a/apps/user_ldap/tests/LDAPUserPluginDummy.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\ILDAPUserPlugin; - -class LDAPUserPluginDummy implements ILDAPUserPlugin { - public function respondToActions() { - return null; - } - - public function createUser($username, $password) { - return null; - } - - public function setPassword($uid, $password) { - return null; - } - - public function getHome($uid) { - return null; - } - - public function getDisplayName($uid) { - return null; - } - - public function setDisplayName($uid, $displayName) { - return null; - } - - public function canChangeAvatar($uid) { - return null; - } - - public function countUsers() { - return null; - } -} diff --git a/apps/user_ldap/tests/Mapping/AbstractMappingTest.php b/apps/user_ldap/tests/Mapping/AbstractMappingTest.php deleted file mode 100644 index e4c37aff7cd29..0000000000000 --- a/apps/user_ldap/tests/Mapping/AbstractMappingTest.php +++ /dev/null @@ -1,303 +0,0 @@ - - * @author Arthur Schiwon - * @author Christoph Wurst - * @author Joas Schilling - * @author Morris Jobke - * @author Roeland Jago Douma - * @author Stefan Weil - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Mapping; - -use OCA\User_LDAP\Mapping\AbstractMapping; -use OCP\IDBConnection; - -abstract class AbstractMappingTest extends \Test\TestCase { - abstract public function getMapper(\OCP\IDBConnection $dbMock); - - /** - * kiss test on isColNameValid - */ - public function testIsColNameValid() { - $dbMock = $this->createMock(IDBConnection::class); - $mapper = $this->getMapper($dbMock); - - $this->assertTrue($mapper->isColNameValid('ldap_dn')); - $this->assertFalse($mapper->isColNameValid('foobar')); - } - - /** - * returns an array of test entries with dn, name and uuid as keys - * @return array - */ - protected function getTestData() { - $data = [ - [ - 'dn' => 'uid=foobar,dc=example,dc=org', - 'name' => 'Foobar', - 'uuid' => '1111-AAAA-1234-CDEF', - ], - [ - 'dn' => 'uid=barfoo,dc=example,dc=org', - 'name' => 'Barfoo', - 'uuid' => '2222-BBBB-1234-CDEF', - ], - [ - 'dn' => 'uid=barabara,dc=example,dc=org', - 'name' => 'BaraBara', - 'uuid' => '3333-CCCC-1234-CDEF', - ] - ]; - - return $data; - } - - /** - * calls map() on the given mapper and asserts result for true - * @param \OCA\User_LDAP\Mapping\AbstractMapping $mapper - * @param array $data - */ - protected function mapEntries($mapper, $data) { - foreach ($data as $entry) { - $done = $mapper->map($entry['dn'], $entry['name'], $entry['uuid']); - $this->assertTrue($done); - } - } - - /** - * initalizes environment for a test run and returns an array with - * test objects. Preparing environment means that all mappings are cleared - * first and then filled with test entries. - * @return array 0 = \OCA\User_LDAP\Mapping\AbstractMapping, 1 = array of - * users or groups - */ - private function initTest() { - $dbc = \OC::$server->getDatabaseConnection(); - $mapper = $this->getMapper($dbc); - $data = $this->getTestData(); - // make sure DB is pristine, then fill it with test entries - $mapper->clear(); - $this->mapEntries($mapper, $data); - - return [$mapper, $data]; - } - - /** - * tests map() method with input that should result in not-mapping. - * Hint: successful mapping is tested inherently with mapEntries(). - */ - public function testMap() { - list($mapper, $data) = $this->initTest(); - - // test that mapping will not happen when it shall not - $tooLongDN = 'uid=joann,ou=Secret Small Specialized Department,ou=Some Tremendously Important Department,ou=Another Very Important Department,ou=Pretty Meaningful Derpartment,ou=Quite Broad And General Department,ou=The Topmost Department,dc=hugelysuccessfulcompany,dc=com'; - $paramKeys = ['', 'dn', 'name', 'uuid', $tooLongDN]; - foreach ($paramKeys as $key) { - $failEntry = $data[0]; - if (!empty($key)) { - $failEntry[$key] = 'do-not-get-mapped'; - } - $isMapped = $mapper->map($failEntry['dn'], $failEntry['name'], $failEntry['uuid']); - $this->assertFalse($isMapped); - } - } - - /** - * tests unmap() for both successful and unsuccessful removing of - * mapping entries - */ - public function testUnmap() { - list($mapper, $data) = $this->initTest(); - - foreach ($data as $entry) { - $result = $mapper->unmap($entry['name']); - $this->assertTrue($result); - } - - $result = $mapper->unmap('notAnEntry'); - $this->assertFalse($result); - } - - /** - * tests getDNByName(), getNameByDN() and getNameByUUID() for successful - * and unsuccessful requests. - */ - public function testGetMethods() { - list($mapper, $data) = $this->initTest(); - - foreach ($data as $entry) { - $fdn = $mapper->getDNByName($entry['name']); - $this->assertSame($fdn, $entry['dn']); - } - $fdn = $mapper->getDNByName('nosuchname'); - $this->assertFalse($fdn); - - foreach ($data as $entry) { - $name = $mapper->getNameByDN($entry['dn']); - $this->assertSame($name, $entry['name']); - } - $name = $mapper->getNameByDN('nosuchdn'); - $this->assertFalse($name); - - foreach ($data as $entry) { - $name = $mapper->getNameByUUID($entry['uuid']); - $this->assertSame($name, $entry['name']); - } - $name = $mapper->getNameByUUID('nosuchuuid'); - $this->assertFalse($name); - } - - /** - * tests getNamesBySearch() for successful and unsuccessful requests. - */ - public function testSearch() { - list($mapper,) = $this->initTest(); - - $names = $mapper->getNamesBySearch('oo', '%', '%'); - $this->assertTrue(is_array($names)); - $this->assertSame(2, count($names)); - $this->assertTrue(in_array('Foobar', $names)); - $this->assertTrue(in_array('Barfoo', $names)); - $names = $mapper->getNamesBySearch('nada'); - $this->assertTrue(is_array($names)); - $this->assertSame(0, count($names)); - } - - /** - * tests setDNbyUUID() for successful and unsuccessful update. - */ - public function testSetDNMethod() { - list($mapper, $data) = $this->initTest(); - - $newDN = 'uid=modified,dc=example,dc=org'; - $done = $mapper->setDNbyUUID($newDN, $data[0]['uuid']); - $this->assertTrue($done); - $fdn = $mapper->getDNByName($data[0]['name']); - $this->assertSame($fdn, $newDN); - - $newDN = 'uid=notme,dc=example,dc=org'; - $done = $mapper->setDNbyUUID($newDN, 'iamnothere'); - $this->assertFalse($done); - $name = $mapper->getNameByDN($newDN); - $this->assertFalse($name); - } - - /** - * tests setUUIDbyDN() for successful and unsuccessful update. - */ - public function testSetUUIDMethod() { - /** @var AbstractMapping $mapper */ - list($mapper, $data) = $this->initTest(); - - $newUUID = 'ABC737-DEF754'; - - $done = $mapper->setUUIDbyDN($newUUID, 'uid=notme,dc=example,dc=org'); - $this->assertFalse($done); - $name = $mapper->getNameByUUID($newUUID); - $this->assertFalse($name); - - $done = $mapper->setUUIDbyDN($newUUID, $data[0]['dn']); - $this->assertTrue($done); - $uuid = $mapper->getUUIDByDN($data[0]['dn']); - $this->assertSame($uuid, $newUUID); - } - - /** - * tests clear() for successful update. - */ - public function testClear() { - list($mapper, $data) = $this->initTest(); - - $done = $mapper->clear(); - $this->assertTrue($done); - foreach ($data as $entry) { - $name = $mapper->getNameByUUID($entry['uuid']); - $this->assertFalse($name); - } - } - - /** - * tests clear() for successful update. - */ - public function testClearCb() { - list($mapper, $data) = $this->initTest(); - - $callbackCalls = 0; - $test = $this; - - $callback = function (string $id) use ($test, &$callbackCalls) { - $test->assertTrue(trim($id) !== ''); - $callbackCalls++; - }; - - $done = $mapper->clearCb($callback, $callback); - $this->assertTrue($done); - $this->assertSame(count($data) * 2, $callbackCalls); - foreach ($data as $entry) { - $name = $mapper->getNameByUUID($entry['uuid']); - $this->assertFalse($name); - } - } - - /** - * tests getList() method - */ - public function testList() { - list($mapper, $data) = $this->initTest(); - - // get all entries without specifying offset or limit - $results = $mapper->getList(); - $this->assertSame(3, count($results)); - - // get all-1 entries by specifying offset, and an high limit - // specifying only offset without limit will not work by underlying lib - $results = $mapper->getList(1, 999); - $this->assertSame(count($data) - 1, count($results)); - - // get first 2 entries by limit, but not offset - $results = $mapper->getList(0, 2); - $this->assertSame(2, count($results)); - - // get 2nd entry by specifying both offset and limit - $results = $mapper->getList(1, 1); - $this->assertSame(1, count($results)); - } - - public function testGetListOfIdsByDn() { - /** @var AbstractMapping $mapper */ - list($mapper,) = $this->initTest(); - - $listOfDNs = []; - for ($i = 0; $i < 66640; $i++) { - // Postgres has a limit of 65535 values in a single IN list - $name = 'as_' . $i; - $dn = 'uid=' . $name . ',dc=example,dc=org'; - $listOfDNs[] = $dn; - if ($i % 20 === 0) { - $mapper->map($dn, $name, 'fake-uuid-' . $i); - } - } - - $result = $mapper->getListOfIdsByDn($listOfDNs); - $this->assertSame(66640 / 20, count($result)); - } -} diff --git a/apps/user_ldap/tests/Mapping/GroupMappingTest.php b/apps/user_ldap/tests/Mapping/GroupMappingTest.php deleted file mode 100644 index 6549ae66671ac..0000000000000 --- a/apps/user_ldap/tests/Mapping/GroupMappingTest.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @author Joas Schilling - * @author Morris Jobke - * @author Thomas Müller - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Mapping; - -use OCA\User_LDAP\Mapping\GroupMapping; - -/** - * Class GroupMappingTest - * - * @group DB - * - * @package OCA\User_LDAP\Tests\Mapping - */ -class GroupMappingTest extends AbstractMappingTest { - public function getMapper(\OCP\IDBConnection $dbMock) { - return new GroupMapping($dbMock); - } -} diff --git a/apps/user_ldap/tests/Mapping/UserMappingTest.php b/apps/user_ldap/tests/Mapping/UserMappingTest.php deleted file mode 100644 index 2417eda0b8cb6..0000000000000 --- a/apps/user_ldap/tests/Mapping/UserMappingTest.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @author Joas Schilling - * @author Morris Jobke - * @author Thomas Müller - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\Mapping; - -use OCA\User_LDAP\Mapping\UserMapping; - -/** - * Class UserMappingTest - * - * @group DB - * - * @package OCA\User_LDAP\Tests\Mapping - */ -class UserMappingTest extends AbstractMappingTest { - public function getMapper(\OCP\IDBConnection $dbMock) { - return new UserMapping($dbMock); - } -} diff --git a/apps/user_ldap/tests/Migration/AbstractUUIDFixTest.php b/apps/user_ldap/tests/Migration/AbstractUUIDFixTest.php deleted file mode 100644 index ec484dfe7eb27..0000000000000 --- a/apps/user_ldap/tests/Migration/AbstractUUIDFixTest.php +++ /dev/null @@ -1,198 +0,0 @@ - - * - * @author Arthur Schiwon - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests\Migration; - -use OCA\User_LDAP\Access; -use OCA\User_LDAP\Helper; -use OCA\User_LDAP\LDAP; -use OCA\User_LDAP\Mapping\GroupMapping; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\Migration\UUIDFixUser; -use OCA\User_LDAP\User_Proxy; -use OCP\IConfig; -use Test\TestCase; - -abstract class AbstractUUIDFixTest extends TestCase { - /** @var Helper|\PHPUnit\Framework\MockObject\MockObject */ - protected $helper; - - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - protected $config; - - /** @var LDAP|\PHPUnit\Framework\MockObject\MockObject */ - protected $ldap; - - /** @var UserMapping|GroupMapping|\PHPUnit\Framework\MockObject\MockObject */ - protected $mapper; - - /** @var UUIDFixUser */ - protected $job; - - /** @var User_Proxy|\PHPUnit\Framework\MockObject\MockObject */ - protected $proxy; - - /** @var Access|\PHPUnit\Framework\MockObject\MockObject */ - protected $access; - - /** @var bool */ - protected $isUser = true; - - protected function setUp(): void { - parent::setUp(); - - $this->ldap = $this->createMock(LDAP::class); - $this->config = $this->createMock(IConfig::class); - $this->access = $this->createMock(Access::class); - - $this->helper = $this->createMock(Helper::class); - $this->helper->expects($this->any()) - ->method('getServerConfigurationPrefixes') - ->with(true) - ->willReturn(['s01', 's03']); - } - - protected function mockProxy($className) { - $this->proxy = $this->createMock($className); - $this->proxy->expects($this->any()) - ->method('getLDAPAccess') - ->willReturn($this->access); - } - - protected function instantiateJob($className) { - $this->job = new $className($this->mapper, $this->ldap, $this->config, $this->helper); - $this->job->overrideProxy($this->proxy); - } - - public function testRunSingleRecord() { - $args = [ - 'records' => [ - 0 => [ - 'name' => 'Someone', - 'dn' => 'uid=Someone,dc=Somewhere', - 'uuid' => 'kaput' - ] - ] - ]; - $correctUUID = '4355-AED3-9D73-03AD'; - - $this->access->expects($this->once()) - ->method('getUUID') - ->with($args['records'][0]['dn'], $this->isUser) - ->willReturn($correctUUID); - - $this->mapper->expects($this->once()) - ->method('setUUIDbyDN') - ->with($correctUUID, $args['records'][0]['dn']); - - $this->job->run($args); - } - - public function testRunValidRecord() { - $correctUUID = '4355-AED3-9D73-03AD'; - $args = [ - 'records' => [ - 0 => [ - 'name' => 'Someone', - 'dn' => 'uid=Someone,dc=Somewhere', - 'uuid' => $correctUUID - ] - ] - ]; - - $this->access->expects($this->once()) - ->method('getUUID') - ->with($args['records'][0]['dn'], $this->isUser) - ->willReturn($correctUUID); - - $this->mapper->expects($this->never()) - ->method('setUUIDbyDN'); - - $this->job->run($args); - } - - public function testRunRemovedRecord() { - $args = [ - 'records' => [ - 0 => [ - 'name' => 'Someone', - 'dn' => 'uid=Someone,dc=Somewhere', - 'uuid' => 'kaput' - ] - ] - ]; - - $this->access->expects($this->once()) - ->method('getUUID') - ->with($args['records'][0]['dn'], $this->isUser) - ->willReturn(false); - - $this->mapper->expects($this->never()) - ->method('setUUIDbyDN'); - - $this->job->run($args); - } - - public function testRunManyRecords() { - $args = [ - 'records' => [ - 0 => [ - 'name' => 'Someone', - 'dn' => 'uid=Someone,dc=Somewhere', - 'uuid' => 'kaput' - ], - 1 => [ - 'name' => 'kdslkdsaIdsal', - 'dn' => 'uid=kdslkdsaIdsal,dc=Somewhere', - 'uuid' => 'AED3-4355-03AD-9D73' - ], - 2 => [ - 'name' => 'Paperboy', - 'dn' => 'uid=Paperboy,dc=Somewhere', - 'uuid' => 'kaput' - ] - ] - ]; - $correctUUIDs = ['4355-AED3-9D73-03AD', 'AED3-4355-03AD-9D73', 'AED3-9D73-4355-03AD']; - - $this->access->expects($this->exactly(3)) - ->method('getUUID') - ->withConsecutive( - [$args['records'][0]['dn'], $this->isUser], - [$args['records'][1]['dn'], $this->isUser], - [$args['records'][2]['dn'], $this->isUser] - ) - ->willReturnOnConsecutiveCalls($correctUUIDs[0], $correctUUIDs[1], $correctUUIDs[2]); - - $this->mapper->expects($this->exactly(2)) - ->method('setUUIDbyDN') - ->withConsecutive( - [$correctUUIDs[0], $args['records'][0]['dn']], - [$correctUUIDs[2], $args['records'][2]['dn']] - ); - - $this->job->run($args); - } -} diff --git a/apps/user_ldap/tests/Migration/UUIDFixGroupTest.php b/apps/user_ldap/tests/Migration/UUIDFixGroupTest.php deleted file mode 100644 index a1f04d44670ed..0000000000000 --- a/apps/user_ldap/tests/Migration/UUIDFixGroupTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * @author Arthur Schiwon - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\Group_LDAP\Tests\Migration; - -use OCA\User_LDAP\Group_Proxy; -use OCA\User_LDAP\Mapping\GroupMapping; -use OCA\User_LDAP\Migration\UUIDFixGroup; -use OCA\User_LDAP\Tests\Migration\AbstractUUIDFixTest; - -/** - * Class UUIDFixGroupTest - * - * @package OCA\Group_LDAP\Tests\Migration - * @group DB - */ -class UUIDFixGroupTest extends AbstractUUIDFixTest { - protected function setUp(): void { - $this->isUser = false; - parent::setUp(); - - $this->isUser = false; - - $this->mapper = $this->createMock(GroupMapping::class); - - $this->mockProxy(Group_Proxy::class); - $this->instantiateJob(UUIDFixGroup::class); - } -} diff --git a/apps/user_ldap/tests/Migration/UUIDFixInsertTest.php b/apps/user_ldap/tests/Migration/UUIDFixInsertTest.php deleted file mode 100644 index 492f0c657ac10..0000000000000 --- a/apps/user_ldap/tests/Migration/UUIDFixInsertTest.php +++ /dev/null @@ -1,197 +0,0 @@ - - * - * @author Arthur Schiwon - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests\Migration; - -use OCA\User_LDAP\Mapping\GroupMapping; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\Migration\UUIDFixInsert; -use OCP\BackgroundJob\IJobList; -use OCP\IConfig; -use OCP\Migration\IOutput; -use Test\TestCase; - -class UUIDFixInsertTest extends TestCase { - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - protected $config; - - /** @var UserMapping|\PHPUnit\Framework\MockObject\MockObject */ - protected $userMapper; - - /** @var GroupMapping|\PHPUnit\Framework\MockObject\MockObject */ - protected $groupMapper; - - /** @var IJobList|\PHPUnit\Framework\MockObject\MockObject */ - protected $jobList; - - /** @var UUIDFixInsert */ - protected $job; - - protected function setUp(): void { - parent::setUp(); - - $this->jobList = $this->createMock(IJobList::class); - $this->config = $this->createMock(IConfig::class); - $this->userMapper = $this->createMock(UserMapping::class); - $this->groupMapper = $this->createMock(GroupMapping::class); - $this->job = new UUIDFixInsert( - $this->config, - $this->userMapper, - $this->groupMapper, - $this->jobList - ); - } - - public function testGetName() { - $this->assertSame('Insert UUIDFix background job for user and group in batches', $this->job->getName()); - } - - public function recordProvider() { - $record = [ - 'dn' => 'cn=somerecord,dc=somewhere', - 'name' => 'Something', - 'uuid' => 'AB12-3456-CDEF7-8GH9' - ]; - array_fill(0, 50, $record); - - $userBatches = [ - 0 => array_fill(0, 50, $record), - 1 => array_fill(0, 50, $record), - 2 => array_fill(0, 13, $record), - ]; - - $groupBatches = [ - 0 => array_fill(0, 7, $record), - ]; - - return [ - ['userBatches' => $userBatches, 'groupBatches' => $groupBatches] - ]; - } - - public function recordProviderTooLongAndNone() { - $record = [ - 'dn' => 'cn=somerecord,dc=somewhere', - 'name' => 'Something', - 'uuid' => 'AB12-3456-CDEF7-8GH9' - ]; - array_fill(0, 50, $record); - - $userBatches = [ - 0 => array_fill(0, 50, $record), - 1 => array_fill(0, 40, $record), - 2 => array_fill(0, 32, $record), - 3 => array_fill(0, 32, $record), - 4 => array_fill(0, 23, $record), - ]; - - $groupBatches = [0 => []]; - - return [ - ['userBatches' => $userBatches, 'groupBatches' => $groupBatches] - ]; - } - - /** - * @dataProvider recordProvider - */ - public function testRun($userBatches, $groupBatches) { - $this->config->expects($this->once()) - ->method('getAppValue') - ->with('user_ldap', 'installed_version', '1.2.1') - ->willReturn('1.2.0'); - - $this->userMapper->expects($this->exactly(3)) - ->method('getList') - ->withConsecutive([0, 50], [50, 50], [100, 50]) - ->willReturnOnConsecutiveCalls($userBatches[0], $userBatches[1], $userBatches[2]); - - $this->groupMapper->expects($this->exactly(1)) - ->method('getList') - ->with(0, 50) - ->willReturn($groupBatches[0]); - - $this->jobList->expects($this->exactly(4)) - ->method('add'); - - /** @var IOutput $out */ - $out = $this->createMock(IOutput::class); - $this->job->run($out); - } - - /** - * @dataProvider recordProviderTooLongAndNone - */ - public function testRunWithManyAndNone($userBatches, $groupBatches) { - $this->config->expects($this->once()) - ->method('getAppValue') - ->with('user_ldap', 'installed_version', '1.2.1') - ->willReturn('1.2.0'); - - $this->userMapper->expects($this->exactly(5)) - ->method('getList') - ->withConsecutive([0, 50], [0, 40], [0, 32], [32, 32], [64, 32]) - ->willReturnOnConsecutiveCalls($userBatches[0], $userBatches[1], $userBatches[2], $userBatches[3], $userBatches[4]); - - $this->groupMapper->expects($this->once()) - ->method('getList') - ->with(0, 50) - ->willReturn($groupBatches[0]); - - $this->jobList->expects($this->at(0)) - ->method('add') - ->willThrowException(new \InvalidArgumentException('Background job arguments can\'t exceed 4000 etc')); - $this->jobList->expects($this->at(1)) - ->method('add') - ->willThrowException(new \InvalidArgumentException('Background job arguments can\'t exceed 4000 etc')); - $this->jobList->expects($this->at(2)) - ->method('add'); - $this->jobList->expects($this->at(3)) - ->method('add'); - $this->jobList->expects($this->at(4)) - ->method('add'); - - /** @var IOutput $out */ - $out = $this->createMock(IOutput::class); - $this->job->run($out); - } - - public function testDonNotRun() { - $this->config->expects($this->once()) - ->method('getAppValue') - ->with('user_ldap', 'installed_version', '1.2.1') - ->willReturn('1.2.1'); - $this->userMapper->expects($this->never()) - ->method('getList'); - $this->groupMapper->expects($this->never()) - ->method('getList'); - $this->jobList->expects($this->never()) - ->method('add'); - - /** @var IOutput $out */ - $out = $this->createMock(IOutput::class); - $this->job->run($out); - } -} diff --git a/apps/user_ldap/tests/Migration/UUIDFixUserTest.php b/apps/user_ldap/tests/Migration/UUIDFixUserTest.php deleted file mode 100644 index 188c7b0f91db4..0000000000000 --- a/apps/user_ldap/tests/Migration/UUIDFixUserTest.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * @author Arthur Schiwon - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests\Migration; - -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\Migration\UUIDFixUser; -use OCA\User_LDAP\User_Proxy; - -/** - * Class UUIDFixUserTest - * - * @package OCA\User_LDAP\Tests\Migration - * @group DB - */ -class UUIDFixUserTest extends AbstractUUIDFixTest { - protected function setUp(): void { - $this->isUser = true; - parent::setUp(); - - $this->mapper = $this->createMock(UserMapping::class); - - $this->mockProxy(User_Proxy::class); - $this->instantiateJob(UUIDFixUser::class); - } -} diff --git a/apps/user_ldap/tests/Settings/AdminTest.php b/apps/user_ldap/tests/Settings/AdminTest.php deleted file mode 100644 index 2599a966f06ed..0000000000000 --- a/apps/user_ldap/tests/Settings/AdminTest.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * @author Arthur Schiwon - * @author Christoph Wurst - * @author Lukas Reschke - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests\Settings; - -use OCA\User_LDAP\Configuration; -use OCA\User_LDAP\Settings\Admin; -use OCP\AppFramework\Http\TemplateResponse; -use OCP\IL10N; -use OCP\Template; -use Test\TestCase; - -/** - * @group DB - * @package OCA\User_LDAP\Tests\Settings - */ -class AdminTest extends TestCase { - /** @var Admin */ - private $admin; - /** @var IL10N */ - private $l10n; - - protected function setUp(): void { - parent::setUp(); - $this->l10n = $this->getMockBuilder(IL10N::class)->getMock(); - - $this->admin = new Admin( - $this->l10n - ); - } - - /** - * @UseDB - */ - public function testGetForm() { - $prefixes = ['s01']; - $hosts = ['s01' => '']; - - $wControls = new Template('user_ldap', 'part.wizardcontrols'); - $wControls = $wControls->fetchPage(); - $sControls = new Template('user_ldap', 'part.settingcontrols'); - $sControls = $sControls->fetchPage(); - - $parameters['serverConfigurationPrefixes'] = $prefixes; - $parameters['serverConfigurationHosts'] = $hosts; - $parameters['settingControls'] = $sControls; - $parameters['wizardControls'] = $wControls; - - // assign default values - $config = new Configuration('', false); - $defaults = $config->getDefaults(); - foreach ($defaults as $key => $default) { - $parameters[$key.'_default'] = $default; - } - - $expected = new TemplateResponse('user_ldap', 'settings', $parameters); - $this->assertEquals($expected, $this->admin->getForm()); - } - - public function testGetSection() { - $this->assertSame('ldap', $this->admin->getSection()); - } - - public function testGetPriority() { - $this->assertSame(5, $this->admin->getPriority()); - } -} diff --git a/apps/user_ldap/tests/Settings/SectionTest.php b/apps/user_ldap/tests/Settings/SectionTest.php deleted file mode 100644 index f27ac0843e82a..0000000000000 --- a/apps/user_ldap/tests/Settings/SectionTest.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * @author Joas Schilling - * @author Lukas Reschke - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests\Settings; - -use OCA\User_LDAP\Settings\Section; -use OCP\IL10N; -use OCP\IURLGenerator; -use Test\TestCase; - -class SectionTest extends TestCase { - /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */ - private $url; - /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ - private $l; - /** @var Section */ - private $section; - - protected function setUp(): void { - parent::setUp(); - $this->url = $this->createMock(IURLGenerator::class); - $this->l = $this->createMock(IL10N::class); - - $this->section = new Section( - $this->url, - $this->l - ); - } - - public function testGetID() { - $this->assertSame('ldap', $this->section->getID()); - } - - public function testGetName() { - $this->l - ->expects($this->once()) - ->method('t') - ->with('LDAP / AD integration') - ->willReturn('LDAP / AD integration'); - - $this->assertSame('LDAP / AD integration', $this->section->getName()); - } - - public function testGetPriority() { - $this->assertSame(25, $this->section->getPriority()); - } - - public function testGetIcon() { - $this->url->expects($this->once()) - ->method('imagePath') - ->with('user_ldap', 'app-dark.svg') - ->willReturn('icon'); - - $this->assertSame('icon', $this->section->getIcon()); - } -} diff --git a/apps/user_ldap/tests/User/DeletedUsersIndexTest.php b/apps/user_ldap/tests/User/DeletedUsersIndexTest.php deleted file mode 100644 index 77fdd31c173bb..0000000000000 --- a/apps/user_ldap/tests/User/DeletedUsersIndexTest.php +++ /dev/null @@ -1,122 +0,0 @@ - - * - * @author Arthur Schiwon - * @author Christoph Wurst - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests\User; - -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\User\DeletedUsersIndex; -use OCP\IConfig; -use OCP\IDBConnection; - -/** - * Class DeletedUsersIndexTest - * - * @group DB - * - * @package OCA\User_LDAP\Tests\User - */ -class DeletedUsersIndexTest extends \Test\TestCase { - /** @var DeletedUsersIndex */ - protected $dui; - - /** @var IConfig */ - protected $config; - - /** @var IDBConnection */ - protected $db; - - /** @var UserMapping|\PHPUnit\Framework\MockObject\MockObject */ - protected $mapping; - - protected function setUp(): void { - parent::setUp(); - - // no mocks for those as tests go against DB - $this->config = \OC::$server->getConfig(); - $this->db = \OC::$server->getDatabaseConnection(); - - // ensure a clean database - $this->config->deleteAppFromAllUsers('user_ldap'); - - $this->mapping = $this->createMock(UserMapping::class); - - $this->dui = new DeletedUsersIndex($this->config, $this->db, $this->mapping); - } - - protected function tearDown(): void { - $this->config->deleteAppFromAllUsers('user_ldap'); - parent::tearDown(); - } - - public function testMarkAndFetchUser() { - $uids = [ - 'cef3775c-71d2-48eb-8984-39a4051b0b95', - '8c4bbb40-33ed-42d0-9b14-85b0ab76c1cc', - ]; - - // ensure test works on a pristine state - $this->assertFalse($this->dui->hasUsers()); - - $this->dui->markUser($uids[0]); - - $this->assertTrue($this->dui->hasUsers()); - - $this->dui->markUser($uids[1]); - - $deletedUsers = $this->dui->getUsers(); - $this->assertSame(2, count($deletedUsers)); - - // ensure the different uids were used - foreach ($deletedUsers as $deletedUser) { - $this->assertTrue(in_array($deletedUser->getOCName(), $uids)); - $i = array_search($deletedUser->getOCName(), $uids); - $this->assertNotFalse($i); - unset($uids[$i]); - } - $this->assertEmpty($uids); - } - - public function testUnmarkUser() { - $uids = [ - '22a162c7-a9ee-487c-9f33-0563795583fb', - '1fb4e0da-4a75-47f3-8fa7-becc7e35c9c5', - ]; - - // we know this works, because of "testMarkAndFetchUser" - $this->dui->markUser($uids[0]); - // this returns a working instance of OfflineUser - $testUser = $this->dui->getUsers()[0]; - $testUser->unmark(); - - // the DUI caches the users, to clear mark someone else - $this->dui->markUser($uids[1]); - - $deletedUsers = $this->dui->getUsers(); - foreach ($deletedUsers as $deletedUser) { - $this->assertNotSame($testUser->getOCName(), $deletedUser->getOCName()); - } - } -} diff --git a/apps/user_ldap/tests/User/ManagerTest.php b/apps/user_ldap/tests/User/ManagerTest.php deleted file mode 100644 index 18499da9a8699..0000000000000 --- a/apps/user_ldap/tests/User/ManagerTest.php +++ /dev/null @@ -1,254 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Morris Jobke - * @author Philippe Jung - * @author Roeland Jago Douma - * @author Roger Szabo - * @author Thomas Müller - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\User; - -use OCA\User_LDAP\Access; -use OCA\User_LDAP\Connection; -use OCA\User_LDAP\FilesystemHelper; -use OCA\User_LDAP\ILDAPWrapper; -use OCA\User_LDAP\LogWrapper; -use OCA\User_LDAP\User\Manager; -use OCA\User_LDAP\User\User; -use OCP\IAvatarManager; -use OCP\IConfig; -use OCP\IDBConnection; -use OCP\Image; -use OCP\IUserManager; -use OCP\Notification\IManager as INotificationManager; - -/** - * Class Test_User_Manager - * - * @group DB - * - * @package OCA\User_LDAP\Tests\User - */ -class ManagerTest extends \Test\TestCase { - /** @var Access|\PHPUnit\Framework\MockObject\MockObject */ - protected $access; - - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - protected $config; - - /** @var FilesystemHelper|\PHPUnit\Framework\MockObject\MockObject */ - protected $fileSystemHelper; - - /** @var LogWrapper|\PHPUnit\Framework\MockObject\MockObject */ - protected $log; - - /** @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $avatarManager; - - /** @var Image|\PHPUnit\Framework\MockObject\MockObject */ - protected $image; - - /** @var IDBConnection|\PHPUnit\Framework\MockObject\MockObject */ - protected $dbc; - - /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $ncUserManager; - - /** @var INotificationManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $notificationManager; - - /** @var ILDAPWrapper|\PHPUnit\Framework\MockObject\MockObject */ - protected $ldapWrapper; - - /** @var Connection */ - protected $connection; - - /** @var Manager */ - protected $manager; - - protected function setUp(): void { - parent::setUp(); - - $this->access = $this->createMock(Access::class); - $this->config = $this->createMock(IConfig::class); - $this->fileSystemHelper = $this->createMock(FilesystemHelper::class); - $this->log = $this->createMock(LogWrapper::class); - $this->avatarManager = $this->createMock(IAvatarManager::class); - $this->image = $this->createMock(Image::class); - $this->dbc = $this->createMock(IDBConnection::class); - $this->ncUserManager = $this->createMock(IUserManager::class); - $this->notificationManager = $this->createMock(INotificationManager::class); - - $this->ldapWrapper = $this->createMock(ILDAPWrapper::class); - $this->connection = new Connection($this->ldapWrapper, '', null); - - $this->access->expects($this->any()) - ->method('getConnection') - ->willReturn($this->connection); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->manager = new Manager( - $this->config, - $this->fileSystemHelper, - $this->log, - $this->avatarManager, - $this->image, - $this->dbc, - $this->ncUserManager, - $this->notificationManager - ); - - $this->manager->setLdapAccess($this->access); - } - - public function dnProvider() { - return [ - ['cn=foo,dc=foobar,dc=bar'], - ['uid=foo,o=foobar,c=bar'], - ['ab=cde,f=ghei,mno=pq'], - ]; - } - - /** - * @dataProvider dnProvider - */ - public function testGetByDNExisting(string $inputDN) { - $uid = '563418fc-423b-1033-8d1c-ad5f418ee02e'; - - $this->access->expects($this->once()) - ->method('stringResemblesDN') - ->with($this->equalTo($inputDN)) - ->willReturn(true); - $this->access->expects($this->once()) - ->method('dn2username') - ->with($this->equalTo($inputDN)) - ->willReturn($uid); - $this->access->expects($this->never()) - ->method('username2dn'); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->manager->get($inputDN); - - // Now we fetch the user again. If this leads to a failing test, - // runtime caching the manager is broken. - /** @noinspection PhpUnhandledExceptionInspection */ - $user = $this->manager->get($inputDN); - - $this->assertInstanceOf(User::class, $user); - } - - public function testGetByDNNotExisting() { - $inputDN = 'cn=gone,dc=foobar,dc=bar'; - - $this->access->expects($this->once()) - ->method('stringResemblesDN') - ->with($this->equalTo($inputDN)) - ->willReturn(true); - $this->access->expects($this->once()) - ->method('dn2username') - ->with($this->equalTo($inputDN)) - ->willReturn(false); - $this->access->expects($this->once()) - ->method('username2dn') - ->with($this->equalTo($inputDN)) - ->willReturn(false); - - /** @noinspection PhpUnhandledExceptionInspection */ - $user = $this->manager->get($inputDN); - - $this->assertNull($user); - } - - public function testGetByUidExisting() { - $dn = 'cn=foo,dc=foobar,dc=bar'; - $uid = '563418fc-423b-1033-8d1c-ad5f418ee02e'; - - $this->access->expects($this->never()) - ->method('dn2username'); - $this->access->expects($this->once()) - ->method('username2dn') - ->with($this->equalTo($uid)) - ->willReturn($dn); - $this->access->expects($this->once()) - ->method('stringResemblesDN') - ->with($this->equalTo($uid)) - ->willReturn(false); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->manager->get($uid); - - // Now we fetch the user again. If this leads to a failing test, - // runtime caching the manager is broken. - /** @noinspection PhpUnhandledExceptionInspection */ - $user = $this->manager->get($uid); - - $this->assertInstanceOf(User::class, $user); - } - - public function testGetByUidNotExisting() { - $uid = 'gone'; - - $this->access->expects($this->never()) - ->method('dn2username'); - $this->access->expects($this->exactly(1)) - ->method('username2dn') - ->with($this->equalTo($uid)) - ->willReturn(false); - - /** @noinspection PhpUnhandledExceptionInspection */ - $user = $this->manager->get($uid); - - $this->assertNull($user); - } - - public function attributeRequestProvider() { - return [ - [false], - [true], - ]; - } - - /** - * @dataProvider attributeRequestProvider - */ - public function testGetAttributes($minimal) { - $this->connection->setConfiguration([ - 'ldapEmailAttribute' => 'MAIL', - 'ldapUserAvatarRule' => 'default', - 'ldapQuotaAttribute' => '', - 'ldapUserDisplayName2' => 'Mail', - ]); - - $attributes = $this->manager->getAttributes($minimal); - - $this->assertTrue(in_array('dn', $attributes)); - $this->assertTrue(in_array(strtolower($this->access->getConnection()->ldapEmailAttribute), $attributes)); - $this->assertTrue(!in_array($this->access->getConnection()->ldapEmailAttribute, $attributes)); #cases check - $this->assertFalse(in_array('', $attributes)); - $this->assertSame(!$minimal, in_array('jpegphoto', $attributes)); - $this->assertSame(!$minimal, in_array('thumbnailphoto', $attributes)); - $valueCounts = array_count_values($attributes); - $this->assertSame(1, $valueCounts['mail']); - } -} diff --git a/apps/user_ldap/tests/User/OfflineUserTest.php b/apps/user_ldap/tests/User/OfflineUserTest.php deleted file mode 100644 index 298e1708a58bc..0000000000000 --- a/apps/user_ldap/tests/User/OfflineUserTest.php +++ /dev/null @@ -1,95 +0,0 @@ - - * - * @author Arthur Schiwon - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests\User; - -use Doctrine\DBAL\Driver\Statement; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\User\OfflineUser; -use OCP\IConfig; -use OCP\IDBConnection; -use Test\TestCase; - -class OfflineUserTest extends TestCase { - - /** @var OfflineUser */ - protected $offlineUser; - /** @var UserMapping|\PHPUnit\Framework\MockObject\MockObject */ - protected $mapping; - /** @var string */ - protected $uid; - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - protected $config; - /** @var IDBConnection|\PHPUnit\Framework\MockObject\MockObject */ - protected $dbc; - - public function setUp(): void { - $this->uid = 'deborah'; - $this->config = $this->createMock(IConfig::class); - $this->dbc = $this->createMock(IDBConnection::class); - $this->mapping = $this->createMock(UserMapping::class); - - $this->offlineUser = new OfflineUser( - $this->uid, - $this->config, - $this->dbc, - $this->mapping - ); - } - - public function shareOwnerProvider(): array { - // tests for none, one, many - return [ - [ 0, 0, false], - [ 1, 0, true], - [ 0, 1, true], - [ 1, 1, true], - [ 2, 0, true], - [ 0, 2, true], - [ 2, 2, true], - ]; - } - - /** - * @dataProvider shareOwnerProvider - */ - public function testHasActiveShares(int $internalOwnerships, int $externalOwnerships, bool $expected) { - $queryMock = $this->createMock(Statement::class); - $queryMock->expects($this->atLeastOnce()) - ->method('execute'); - $queryMock->expects($this->atLeastOnce()) - ->method('rowCount') - ->willReturnOnConsecutiveCalls( - $internalOwnerships > 0 ? 1 : 0, - $externalOwnerships > 0 ? 1 : 0 - ); - - $this->dbc->expects($this->atLeastOnce()) - ->method('prepare') - ->willReturn($queryMock); - - $this->assertSame($expected, $this->offlineUser->getHasActiveShares()); - } -} diff --git a/apps/user_ldap/tests/User/UserTest.php b/apps/user_ldap/tests/User/UserTest.php deleted file mode 100644 index 25a896aee04ae..0000000000000 --- a/apps/user_ldap/tests/User/UserTest.php +++ /dev/null @@ -1,1230 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Juan Pablo Villafáñez - * @author Morris Jobke - * @author Roeland Jago Douma - * @author Roger Szabo - * @author Thomas Müller - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests\User; - -use OCA\User_LDAP\Access; -use OCA\User_LDAP\Connection; -use OCA\User_LDAP\FilesystemHelper; -use OCA\User_LDAP\LogWrapper; -use OCA\User_LDAP\User\User; -use OCP\IAvatar; -use OCP\IAvatarManager; -use OCP\IConfig; -use OCP\Image; -use OCP\IUser; -use OCP\IUserManager; -use OCP\Notification\IManager as INotificationManager; -use OCP\Notification\INotification; - -/** - * Class UserTest - * - * @group DB - * - * @package OCA\User_LDAP\Tests\User - */ -class UserTest extends \Test\TestCase { - /** @var Access|\PHPUnit\Framework\MockObject\MockObject */ - protected $access; - /** @var Connection|\PHPUnit\Framework\MockObject\MockObject */ - protected $connection; - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - protected $config; - /** @var FilesystemHelper|\PHPUnit\Framework\MockObject\MockObject */ - protected $filesystemhelper; - /** @var INotificationManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $notificationManager; - /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $userManager; - /** @var Image|\PHPUnit\Framework\MockObject\MockObject */ - protected $image; - /** @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $avatarManager; - /** @var LogWrapper|\PHPUnit\Framework\MockObject\MockObject */ - protected $log; - /** @var string */ - protected $uid = 'alice'; - /** @var string */ - protected $dn = 'uid=alice,dc=foo,dc=bar'; - /** @var User */ - protected $user; - - protected function setUp(): void { - parent::setUp(); - - $this->connection = $this->createMock(Connection::class); - - $this->access = $this->createMock(Access::class); - $this->access->connection = $this->connection; - $this->access->expects($this->any()) - ->method('getConnection') - ->willReturn($this->connection); - - $this->config = $this->createMock(IConfig::class); - $this->filesystemhelper = $this->createMock(FilesystemHelper::class); - $this->log = $this->createMock(LogWrapper::class); - $this->avatarManager = $this->createMock(IAvatarManager::class); - $this->image = $this->createMock(Image::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->notificationManager = $this->createMock(INotificationManager::class); - - $this->user = new User( - $this->uid, - $this->dn, - $this->access, - $this->config, - $this->filesystemhelper, - $this->image, - $this->log, - $this->avatarManager, - $this->userManager, - $this->notificationManager - ); - } - - public function testGetDNandUsername() { - $this->assertSame($this->dn, $this->user->getDN()); - $this->assertSame($this->uid, $this->user->getUsername()); - } - - public function testUpdateEmailProvided() { - $this->connection->expects($this->once()) - ->method('__get') - ->with($this->equalTo('ldapEmailAttribute')) - ->willReturn('email'); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('email')) - ->willReturn(['alice@foo.bar']); - - $coreUser = $this->getMockBuilder(IUser::class) - ->disableOriginalConstructor() - ->getMock(); - $coreUser->expects($this->once()) - ->method('setEMailAddress') - ->with('alice@foo.bar'); - - $this->userManager->expects($this->any()) - ->method('get') - ->willReturn($coreUser); - - $this->user->updateEmail(); - } - - public function testUpdateEmailNotProvided() { - $this->connection->expects($this->once()) - ->method('__get') - ->with($this->equalTo('ldapEmailAttribute')) - ->willReturn('email'); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('email')) - ->willReturn(false); - - $this->config->expects($this->never()) - ->method('setUserValue'); - - $this->user->updateEmail(); - } - - public function testUpdateEmailNotConfigured() { - $this->connection->expects($this->once()) - ->method('__get') - ->with($this->equalTo('ldapEmailAttribute')) - ->willReturn(''); - - $this->access->expects($this->never()) - ->method('readAttribute'); - - $this->config->expects($this->never()) - ->method('setUserValue'); - - $this->user->updateEmail(); - } - - public function testUpdateQuotaAllProvided() { - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', 'myquota'], - ['ldapQuotaDefault', ''] - ]); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('myquota')) - ->willReturn(['42 GB']); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->once()) - ->method('setQuota') - ->with('42 GB'); - - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->with($this->uid) - ->willReturn($coreUser); - - $this->user->updateQuota(); - } - - public function testUpdateQuotaToDefaultAllProvided() { - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', 'myquota'], - ['ldapQuotaDefault', ''] - ]); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('myquota')) - ->willReturn(['default']); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->once()) - ->method('setQuota') - ->with('default'); - - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->uid) - ->willReturn($coreUser); - - $this->user->updateQuota(); - } - - public function testUpdateQuotaToNoneAllProvided() { - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', 'myquota'], - ['ldapQuotaDefault', ''] - ]); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('myquota')) - ->willReturn(['none']); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->once()) - ->method('setQuota') - ->with('none'); - - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->uid) - ->willReturn($coreUser); - - $this->user->updateQuota(); - } - - public function testUpdateQuotaDefaultProvided() { - $this->connection->expects($this->at(0)) - ->method('__get') - ->with($this->equalTo('ldapQuotaAttribute')) - ->willReturn('myquota'); - $this->connection->expects($this->at(1)) - ->method('__get') - ->with($this->equalTo('ldapQuotaDefault')) - ->willReturn('25 GB'); - $this->connection->expects($this->exactly(2)) - ->method('__get'); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('myquota')) - ->willReturn(false); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->once()) - ->method('setQuota') - ->with('25 GB'); - - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->uid) - ->willReturn($coreUser); - - $this->user->updateQuota(); - } - - public function testUpdateQuotaIndividualProvided() { - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', 'myquota'], - ['ldapQuotaDefault', ''] - ]); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('myquota')) - ->willReturn(['27 GB']); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->once()) - ->method('setQuota') - ->with('27 GB'); - - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->uid) - ->willReturn($coreUser); - - $this->user->updateQuota(); - } - - public function testUpdateQuotaNoneProvided() { - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', 'myquota'], - ['ldapQuotaDefault', ''] - ]); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('myquota')) - ->willReturn(false); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->never()) - ->method('setQuota'); - - $this->userManager->expects($this->never()) - ->method('get') - ->with($this->uid); - - $this->config->expects($this->never()) - ->method('setUserValue'); - - $this->user->updateQuota(); - } - - public function testUpdateQuotaNoneConfigured() { - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', ''], - ['ldapQuotaDefault', ''] - ]); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->never()) - ->method('setQuota'); - - $this->userManager->expects($this->never()) - ->method('get'); - - $this->access->expects($this->never()) - ->method('readAttribute'); - - $this->config->expects($this->never()) - ->method('setUserValue'); - - $this->user->updateQuota(); - } - - public function testUpdateQuotaFromValue() { - $readQuota = '19 GB'; - - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', 'myquota'], - ['ldapQuotaDefault', ''] - ]); - - $this->access->expects($this->never()) - ->method('readAttribute'); - - $user = $this->createMock(IUser::class); - $user->expects($this->once()) - ->method('setQuota') - ->with($readQuota); - - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->uid) - ->willReturn($user); - - $this->user->updateQuota($readQuota); - } - - /** - * Unparseable quota will fallback to use the LDAP default - */ - public function testUpdateWrongQuotaAllProvided() { - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', 'myquota'], - ['ldapQuotaDefault', '23 GB'] - ]); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('myquota')) - ->willReturn(['42 GBwos']); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->once()) - ->method('setQuota') - ->with('23 GB'); - - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->uid) - ->willReturn($coreUser); - - $this->user->updateQuota(); - } - - /** - * No user quota and wrong default will set 'default' as quota - */ - public function testUpdateWrongDefaultQuotaProvided() { - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', 'myquota'], - ['ldapQuotaDefault', '23 GBwowowo'] - ]); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('myquota')) - ->willReturn(false); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->never()) - ->method('setQuota'); - - $this->userManager->expects($this->never()) - ->method('get'); - - $this->user->updateQuota(); - } - - /** - * Wrong user quota and wrong default will set 'default' as quota - */ - public function testUpdateWrongQuotaAndDefaultAllProvided() { - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', 'myquota'], - ['ldapQuotaDefault', '23 GBwowowo'] - ]); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('myquota')) - ->willReturn(['23 flush']); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->never()) - ->method('setQuota'); - - $this->userManager->expects($this->never()) - ->method('get'); - - $this->user->updateQuota(); - } - - /** - * No quota attribute set and wrong default will set 'default' as quota - */ - public function testUpdateWrongDefaultQuotaNotProvided() { - $this->connection->expects($this->exactly(2)) - ->method('__get') - ->willReturnMap([ - ['ldapQuotaAttribute', ''], - ['ldapQuotaDefault', '23 GBwowowo'] - ]); - - $this->access->expects($this->never()) - ->method('readAttribute'); - - $coreUser = $this->createMock(IUser::class); - $coreUser->expects($this->never()) - ->method('setQuota'); - - $this->userManager->expects($this->never()) - ->method('get'); - - $this->user->updateQuota(); - } - - //the testUpdateAvatar series also implicitely tests getAvatarImage - public function XtestUpdateAvatarJpegPhotoProvided() { - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('jpegphoto')) - ->willReturn(['this is a photo']); - - $this->image->expects($this->once()) - ->method('loadFromBase64') - ->willReturn('imageResource'); - $this->image->expects($this->once()) - ->method('valid') - ->willReturn(true); - $this->image->expects($this->once()) - ->method('width') - ->willReturn(128); - $this->image->expects($this->once()) - ->method('height') - ->willReturn(128); - $this->image->expects($this->once()) - ->method('centerCrop') - ->willReturn(true); - $this->image->expects($this->once()) - ->method('data') - ->willReturn('this is a photo'); - - $this->config->expects($this->once()) - ->method('getUserValue') - ->with($this->uid, 'user_ldap', 'lastAvatarChecksum', '') - ->willReturn(''); - $this->config->expects($this->once()) - ->method('setUserValue') - ->with($this->uid, 'user_ldap', 'lastAvatarChecksum', md5('this is a photo')); - - $this->filesystemhelper->expects($this->once()) - ->method('isLoaded') - ->willReturn(true); - - $avatar = $this->createMock(IAvatar::class); - $avatar->expects($this->once()) - ->method('set') - ->with($this->isInstanceOf($this->image)); - - $this->avatarManager->expects($this->once()) - ->method('getAvatar') - ->with($this->equalTo($this->uid)) - ->willReturn($avatar); - - $this->connection->expects($this->any()) - ->method('resolveRule') - ->with('avatar') - ->willReturn(['jpegphoto', 'thumbnailphoto']); - - $this->user->updateAvatar(); - } - - public function testUpdateAvatarKnownJpegPhotoProvided() { - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('jpegphoto')) - ->willReturn(['this is a photo']); - - $this->image->expects($this->once()) - ->method('loadFromBase64') - ->willReturn('imageResource'); - $this->image->expects($this->never()) - ->method('valid'); - $this->image->expects($this->never()) - ->method('width'); - $this->image->expects($this->never()) - ->method('height'); - $this->image->expects($this->never()) - ->method('centerCrop'); - $this->image->expects($this->once()) - ->method('data') - ->willReturn('this is a photo'); - - $this->config->expects($this->once()) - ->method('getUserValue') - ->with($this->uid, 'user_ldap', 'lastAvatarChecksum', '') - ->willReturn(md5('this is a photo')); - $this->config->expects($this->never()) - ->method('setUserValue'); - - $this->filesystemhelper->expects($this->never()) - ->method('isLoaded'); - - $avatar = $this->createMock(IAvatar::class); - $avatar->expects($this->never()) - ->method('set'); - - $this->avatarManager->expects($this->never()) - ->method('getAvatar'); - - $this->connection->expects($this->any()) - ->method('resolveRule') - ->with('avatar') - ->willReturn(['jpegphoto', 'thumbnailphoto']); - - $this->assertTrue($this->user->updateAvatar()); - } - - public function XtestUpdateAvatarThumbnailPhotoProvided() { - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - if ($dn === $this->dn - && $attr === 'jpegphoto') { - return false; - } elseif ($dn === $this->dn - && $attr === 'thumbnailphoto') { - return ['this is a photo']; - } - return null; - }); - - $this->image->expects($this->once()) - ->method('loadFromBase64') - ->willReturn('imageResource'); - $this->image->expects($this->once()) - ->method('valid') - ->willReturn(true); - $this->image->expects($this->once()) - ->method('width') - ->willReturn(128); - $this->image->expects($this->once()) - ->method('height') - ->willReturn(128); - $this->image->expects($this->once()) - ->method('centerCrop') - ->willReturn(true); - $this->image->expects($this->once()) - ->method('data') - ->willReturn('this is a photo'); - - $this->config->expects($this->once()) - ->method('getUserValue') - ->with($this->uid, 'user_ldap', 'lastAvatarChecksum', '') - ->willReturn(''); - $this->config->expects($this->once()) - ->method('setUserValue') - ->with($this->uid, 'user_ldap', 'lastAvatarChecksum', md5('this is a photo')); - - $this->filesystemhelper->expects($this->once()) - ->method('isLoaded') - ->willReturn(true); - - $avatar = $this->createMock(IAvatar::class); - $avatar->expects($this->once()) - ->method('set') - ->with($this->isInstanceOf($this->image)); - - $this->avatarManager->expects($this->once()) - ->method('getAvatar') - ->with($this->equalTo($this->uid)) - ->willReturn($avatar); - - $this->connection->expects($this->any()) - ->method('resolveRule') - ->with('avatar') - ->willReturn(['jpegphoto', 'thumbnailphoto']); - - $this->user->updateAvatar(); - } - - public function testUpdateAvatarCorruptPhotoProvided() { - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - if ($dn === $this->dn - && $attr === 'jpegphoto') { - return false; - } elseif ($dn === $this->dn - && $attr === 'thumbnailphoto') { - return ['this is a photo']; - } - return null; - }); - - $this->image->expects($this->once()) - ->method('loadFromBase64') - ->willReturn(false); - $this->image->expects($this->never()) - ->method('valid'); - $this->image->expects($this->never()) - ->method('width'); - $this->image->expects($this->never()) - ->method('height'); - $this->image->expects($this->never()) - ->method('centerCrop'); - $this->image->expects($this->never()) - ->method('data'); - - $this->config->expects($this->never()) - ->method('getUserValue'); - $this->config->expects($this->never()) - ->method('setUserValue'); - - $this->filesystemhelper->expects($this->never()) - ->method('isLoaded'); - - $avatar = $this->createMock(IAvatar::class); - $avatar->expects($this->never()) - ->method('set'); - - $this->avatarManager->expects($this->never()) - ->method('getAvatar'); - - $this->connection->expects($this->any()) - ->method('resolveRule') - ->with('avatar') - ->willReturn(['jpegphoto', 'thumbnailphoto']); - - $this->user->updateAvatar(); - } - - public function XtestUpdateAvatarUnsupportedThumbnailPhotoProvided() { - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - if ($dn === $this->dn - && $attr === 'jpegphoto') { - return false; - } elseif ($dn === $this->dn - && $attr === 'thumbnailphoto') { - return ['this is a photo']; - } - return null; - }); - - $this->image->expects($this->once()) - ->method('loadFromBase64') - ->willReturn('imageResource'); - $this->image->expects($this->once()) - ->method('valid') - ->willReturn(true); - $this->image->expects($this->once()) - ->method('width') - ->willReturn(128); - $this->image->expects($this->once()) - ->method('height') - ->willReturn(128); - $this->image->expects($this->once()) - ->method('centerCrop') - ->willReturn(true); - $this->image->expects($this->once()) - ->method('data') - ->willReturn('this is a photo'); - - $this->config->expects($this->once()) - ->method('getUserValue') - ->with($this->uid, 'user_ldap', 'lastAvatarChecksum', '') - ->willReturn(''); - $this->config->expects($this->never()) - ->method('setUserValue'); - - $this->filesystemhelper->expects($this->once()) - ->method('isLoaded') - ->willReturn(true); - - $avatar = $this->createMock(IAvatar::class); - $avatar->expects($this->once()) - ->method('set') - ->with($this->isInstanceOf($this->image)) - ->willThrowException(new \Exception()); - - $this->avatarManager->expects($this->once()) - ->method('getAvatar') - ->with($this->equalTo($this->uid)) - ->willReturn($avatar); - - $this->connection->expects($this->any()) - ->method('resolveRule') - ->with('avatar') - ->willReturn(['jpegphoto', 'thumbnailphoto']); - - $this->assertFalse($this->user->updateAvatar()); - } - - public function testUpdateAvatarNotProvided() { - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - if ($dn === $this->dn - && $attr === 'jpegPhoto') { - return false; - } elseif ($dn === $this->dn - && $attr === 'thumbnailPhoto') { - return false; - } - return null; - }); - - $this->image->expects($this->never()) - ->method('valid'); - $this->image->expects($this->never()) - ->method('width'); - $this->image->expects($this->never()) - ->method('height'); - $this->image->expects($this->never()) - ->method('centerCrop'); - $this->image->expects($this->never()) - ->method('data'); - - $this->config->expects($this->never()) - ->method('getUserValue'); - $this->config->expects($this->never()) - ->method('setUserValue'); - - $this->filesystemhelper->expects($this->never()) - ->method('isLoaded'); - - $this->avatarManager->expects($this->never()) - ->method('getAvatar'); - - $this->connection->expects($this->any()) - ->method('resolveRule') - ->with('avatar') - ->willReturn(['jpegphoto', 'thumbnailphoto']); - - $this->user->updateAvatar(); - } - - public function extStorageHomeDataProvider() { - return [ - [ 'myFolder', null ], - [ '', null, false ], - [ 'myFolder', 'myFolder' ], - ]; - } - - /** - * @dataProvider extStorageHomeDataProvider - */ - public function testUpdateExtStorageHome(string $expected, string $valueFromLDAP = null, bool $isSet = true) { - if ($valueFromLDAP === null) { - $this->connection->expects($this->once()) - ->method('__get') - ->willReturnMap([ - ['ldapExtStorageHomeAttribute', 'homeDirectory'], - ]); - - $return = []; - if ($isSet) { - $return[] = $expected; - } - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->dn, 'homeDirectory') - ->willReturn($return); - } - - if ($expected !== '') { - $this->config->expects($this->once()) - ->method('setUserValue') - ->with($this->uid, 'user_ldap', 'extStorageHome', $expected); - } else { - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with($this->uid, 'user_ldap', 'extStorageHome'); - } - - $actual = $this->user->updateExtStorageHome($valueFromLDAP); - $this->assertSame($expected, $actual); - } - - public function testMarkLogin() { - $this->config->expects($this->once()) - ->method('setUserValue') - ->with($this->equalTo($this->uid), - $this->equalTo('user_ldap'), - $this->equalTo(User::USER_PREFKEY_FIRSTLOGIN), - $this->equalTo(1)) - ->willReturn(true); - - $this->user->markLogin(); - } - - public function testGetAvatarImageProvided() { - $this->access->expects($this->once()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), - $this->equalTo('jpegphoto')) - ->willReturn(['this is a photo']); - $this->connection->expects($this->any()) - ->method('resolveRule') - ->with('avatar') - ->willReturn(['jpegphoto', 'thumbnailphoto']); - - $photo = $this->user->getAvatarImage(); - $this->assertSame('this is a photo', $photo); - //make sure readAttribute is not called again but the already fetched - //photo is returned - $this->user->getAvatarImage(); - } - - public function testGetAvatarImageDisabled() { - $this->access->expects($this->never()) - ->method('readAttribute') - ->with($this->equalTo($this->dn), $this->anything()); - $this->connection->expects($this->any()) - ->method('resolveRule') - ->with('avatar') - ->willReturn([]); - - $this->assertFalse($this->user->getAvatarImage()); - } - - public function imageDataProvider() { - return [ - [ false, false ], - [ 'corruptData', false ], - [ 'validData', true ], - ]; - } - - public function testProcessAttributes() { - $requiredMethods = [ - 'updateQuota', - 'updateEmail', - 'composeAndStoreDisplayName', - 'storeLDAPUserName', - 'getHomePath', - 'updateAvatar', - 'updateExtStorageHome', - ]; - - /** @var User|\PHPUnit\Framework\MockObject\MockObject $userMock */ - $userMock = $this->getMockBuilder(User::class) - ->setConstructorArgs([ - $this->uid, - $this->dn, - $this->access, - $this->config, - $this->filesystemhelper, - $this->image, - $this->log, - $this->avatarManager, - $this->userManager, - $this->notificationManager - ]) - ->setMethods($requiredMethods) - ->getMock(); - - $this->connection->setConfiguration([ - 'homeFolderNamingRule' => 'homeDirectory' - ]); - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'homeFolderNamingRule') { - return 'attr:homeDirectory'; - } - return $name; - }); - $this->connection->expects($this->any()) - ->method('resolveRule') - ->with('avatar') - ->willReturn(['jpegphoto', 'thumbnailphoto']); - - $record = [ - strtolower($this->connection->ldapQuotaAttribute) => ['4096'], - strtolower($this->connection->ldapEmailAttribute) => ['alice@wonderland.org'], - strtolower($this->connection->ldapUserDisplayName) => ['Aaaaalice'], - strtolower($this->connection->ldapExtStorageHomeAttribute) => ['homeDirectory'], - 'uid' => [$this->uid], - 'homedirectory' => ['Alice\'s Folder'], - 'memberof' => ['cn=groupOne', 'cn=groupTwo'], - 'jpegphoto' => ['here be an image'] - ]; - - foreach ($requiredMethods as $method) { - $userMock->expects($this->once()) - ->method($method); - } - \OC_Hook::clear();//disconnect irrelevant hooks - $userMock->processAttributes($record); - /** @noinspection PhpUnhandledExceptionInspection */ - \OC_Hook::emit('OC_User', 'post_login', ['uid' => $this->uid]); - } - - public function emptyHomeFolderAttributeValueProvider() { - return [ - 'empty' => [''], - 'prefixOnly' => ['attr:'], - ]; - } - - /** - * @dataProvider emptyHomeFolderAttributeValueProvider - */ - public function testGetHomePathNotConfigured($attributeValue) { - $this->connection->expects($this->any()) - ->method('__get') - ->with($this->equalTo('homeFolderNamingRule')) - ->willReturn($attributeValue); - - $this->access->expects($this->never()) - ->method('readAttribute'); - - $this->config->expects($this->never()) - ->method('getAppValue'); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->assertFalse($this->user->getHomePath()); - } - - public function testGetHomePathConfiguredNotAvailableAllowed() { - $this->connection->expects($this->any()) - ->method('__get') - ->with($this->equalTo('homeFolderNamingRule')) - ->willReturn('attr:foobar'); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->willReturn(false); - - // asks for "enforce_home_folder_naming_rule" - $this->config->expects($this->once()) - ->method('getAppValue') - ->willReturn(false); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->assertFalse($this->user->getHomePath()); - } - - - public function testGetHomePathConfiguredNotAvailableNotAllowed() { - $this->expectException(\Exception::class); - - $this->connection->expects($this->any()) - ->method('__get') - ->with($this->equalTo('homeFolderNamingRule')) - ->willReturn('attr:foobar'); - - $this->access->expects($this->once()) - ->method('readAttribute') - ->willReturn(false); - - // asks for "enforce_home_folder_naming_rule" - $this->config->expects($this->once()) - ->method('getAppValue') - ->willReturn(true); - - $this->user->getHomePath(); - } - - public function displayNameProvider() { - return [ - ['Roland Deschain', '', 'Roland Deschain', false], - ['Roland Deschain', '', 'Roland Deschain', true], - ['Roland Deschain', null, 'Roland Deschain', false], - ['Roland Deschain', 'gunslinger@darktower.com', 'Roland Deschain (gunslinger@darktower.com)', false], - ['Roland Deschain', 'gunslinger@darktower.com', 'Roland Deschain (gunslinger@darktower.com)', true], - ]; - } - - /** - * @dataProvider displayNameProvider - */ - public function testComposeAndStoreDisplayName($part1, $part2, $expected, $expectTriggerChange) { - $this->config->expects($this->once()) - ->method('setUserValue'); - $oldName = $expectTriggerChange ? 'xxGunslingerxx' : null; - $this->config->expects($this->once()) - ->method('getUserValue') - ->with($this->user->getUsername(), 'user_ldap', 'displayName', null) - ->willReturn($oldName); - - $ncUserObj = $this->createMock(\OC\User\User::class); - if ($expectTriggerChange) { - $ncUserObj->expects($this->once()) - ->method('triggerChange') - ->with('displayName', $expected); - } else { - $ncUserObj->expects($this->never()) - ->method('triggerChange'); - } - $this->userManager->expects($this->once()) - ->method('get') - ->willReturn($ncUserObj); - - $displayName = $this->user->composeAndStoreDisplayName($part1, $part2); - $this->assertSame($expected, $displayName); - } - - public function testComposeAndStoreDisplayNameNoOverwrite() { - $displayName = 'Randall Flagg'; - $this->config->expects($this->never()) - ->method('setUserValue'); - $this->config->expects($this->once()) - ->method('getUserValue') - ->willReturn($displayName); - - $this->userManager->expects($this->never()) - ->method('get'); // Implicit: no triggerChange can be called - - $composedDisplayName = $this->user->composeAndStoreDisplayName($displayName); - $this->assertSame($composedDisplayName, $displayName); - } - - public function testHandlePasswordExpiryWarningDefaultPolicy() { - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapDefaultPPolicyDN') { - return 'cn=default,ou=policies,dc=foo,dc=bar'; - } - if ($name === 'turnOnPasswordChange') { - return '1'; - } - return $name; - }); - - $this->access->expects($this->any()) - ->method('search') - ->willReturnCallback(function ($filter, $base) { - if ($base === $this->dn) { - return [ - [ - 'pwdchangedtime' => [(new \DateTime())->sub(new \DateInterval('P28D'))->format('Ymdhis').'Z'], - 'pwdgraceusetime' => [], - ], - ]; - } - if ($base === 'cn=default,ou=policies,dc=foo,dc=bar') { - return [ - [ - 'pwdmaxage' => ['2592000'], - 'pwdexpirewarning' => ['2591999'], - ], - ]; - } - return []; - }); - - $notification = $this->getMockBuilder(INotification::class) - ->disableOriginalConstructor() - ->getMock(); - $notification->expects($this->any()) - ->method('setApp') - ->willReturn($notification); - $notification->expects($this->any()) - ->method('setUser') - ->willReturn($notification); - $notification->expects($this->any()) - ->method('setObject') - ->willReturn($notification); - $notification->expects($this->any()) - ->method('setDateTime') - ->willReturn($notification); - - $this->notificationManager->expects($this->exactly(2)) - ->method('createNotification') - ->willReturn($notification); - $this->notificationManager->expects($this->exactly(1)) - ->method('notify'); - - \OC_Hook::clear();//disconnect irrelevant hooks - \OCP\Util::connectHook('OC_User', 'post_login', $this->user, 'handlePasswordExpiry'); - /** @noinspection PhpUnhandledExceptionInspection */ - \OC_Hook::emit('OC_User', 'post_login', ['uid' => $this->uid]); - } - - public function testHandlePasswordExpiryWarningCustomPolicy() { - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapDefaultPPolicyDN') { - return 'cn=default,ou=policies,dc=foo,dc=bar'; - } - if ($name === 'turnOnPasswordChange') { - return '1'; - } - return $name; - }); - - $this->access->expects($this->any()) - ->method('search') - ->willReturnCallback(function ($filter, $base) { - if ($base === $this->dn) { - return [ - [ - 'pwdpolicysubentry' => ['cn=custom,ou=policies,dc=foo,dc=bar'], - 'pwdchangedtime' => [(new \DateTime())->sub(new \DateInterval('P28D'))->format('Ymdhis').'Z'], - 'pwdgraceusetime' => [], - ] - ]; - } - if ($base === 'cn=custom,ou=policies,dc=foo,dc=bar') { - return [ - [ - 'pwdmaxage' => ['2592000'], - 'pwdexpirewarning' => ['2591999'], - ] - ]; - } - return []; - }); - - $notification = $this->getMockBuilder(INotification::class) - ->disableOriginalConstructor() - ->getMock(); - $notification->expects($this->any()) - ->method('setApp') - ->willReturn($notification); - $notification->expects($this->any()) - ->method('setUser') - ->willReturn($notification); - $notification->expects($this->any()) - ->method('setObject') - ->willReturn($notification); - $notification->expects($this->any()) - ->method('setDateTime') - ->willReturn($notification); - - $this->notificationManager->expects($this->exactly(2)) - ->method('createNotification') - ->willReturn($notification); - $this->notificationManager->expects($this->exactly(1)) - ->method('notify'); - - \OC_Hook::clear();//disconnect irrelevant hooks - \OCP\Util::connectHook('OC_User', 'post_login', $this->user, 'handlePasswordExpiry'); - /** @noinspection PhpUnhandledExceptionInspection */ - \OC_Hook::emit('OC_User', 'post_login', ['uid' => $this->uid]); - } -} diff --git a/apps/user_ldap/tests/UserLDAPPluginTest.php b/apps/user_ldap/tests/UserLDAPPluginTest.php deleted file mode 100644 index e1b3cd9215969..0000000000000 --- a/apps/user_ldap/tests/UserLDAPPluginTest.php +++ /dev/null @@ -1,312 +0,0 @@ - - * @author Roeland Jago Douma - * @author Vinicius Cubas Brand - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests; - -use OC\User\Backend; -use OCA\User_LDAP\UserPluginManager; - -class UserLDAPPluginTest extends \Test\TestCase { - - /** - * @return UserPluginManager - */ - private function getUserPluginManager() { - return new UserPluginManager(); - } - - public function testImplementsActions() { - $pluginManager = $this->getUserPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPUserPluginDummy') - ->setMethods(['respondToActions']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(Backend::CREATE_USER); - - $plugin2 = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPUserPluginDummy') - ->setMethods(['respondToActions']) - ->getMock(); - - $plugin2->expects($this->any()) - ->method('respondToActions') - ->willReturn(Backend::PROVIDE_AVATAR); - - $pluginManager->register($plugin); - $pluginManager->register($plugin2); - - $this->assertEquals($pluginManager->getImplementedActions(), Backend::CREATE_USER | Backend::PROVIDE_AVATAR); - $this->assertTrue($pluginManager->implementsActions(Backend::CREATE_USER)); - $this->assertTrue($pluginManager->implementsActions(Backend::PROVIDE_AVATAR)); - } - - public function testCreateUser() { - $pluginManager = $this->getUserPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPUserPluginDummy') - ->setMethods(['respondToActions', 'createUser']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(Backend::CREATE_USER); - - $plugin->expects($this->once()) - ->method('createUser') - ->with( - $this->equalTo('user'), - $this->equalTo('password') - ); - - $pluginManager->register($plugin); - $pluginManager->createUser('user', 'password'); - } - - - public function testCreateUserNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements createUser in this LDAP Backend.'); - - $pluginManager = $this->getUserPluginManager(); - $pluginManager->createUser('foo','bar'); - } - - public function testSetPassword() { - $pluginManager = $this->getUserPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPUserPluginDummy') - ->setMethods(['respondToActions', 'setPassword']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(Backend::SET_PASSWORD); - - $plugin->expects($this->once()) - ->method('setPassword') - ->with( - $this->equalTo('user'), - $this->equalTo('password') - ); - - $pluginManager->register($plugin); - $pluginManager->setPassword('user', 'password'); - } - - - public function testSetPasswordNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements setPassword in this LDAP Backend.'); - - $pluginManager = $this->getUserPluginManager(); - $pluginManager->setPassword('foo','bar'); - } - - public function testGetHome() { - $pluginManager = $this->getUserPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPUserPluginDummy') - ->setMethods(['respondToActions', 'getHome']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(Backend::GET_HOME); - - $plugin->expects($this->once()) - ->method('getHome') - ->with( - $this->equalTo('uid') - ); - - $pluginManager->register($plugin); - $pluginManager->getHome('uid'); - } - - - public function testGetHomeNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements getHome in this LDAP Backend.'); - - $pluginManager = $this->getUserPluginManager(); - $pluginManager->getHome('foo'); - } - - public function testGetDisplayName() { - $pluginManager = $this->getUserPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPUserPluginDummy') - ->setMethods(['respondToActions', 'getDisplayName']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(Backend::GET_DISPLAYNAME); - - $plugin->expects($this->once()) - ->method('getDisplayName') - ->with( - $this->equalTo('uid') - ); - - $pluginManager->register($plugin); - $pluginManager->getDisplayName('uid'); - } - - - public function testGetDisplayNameNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements getDisplayName in this LDAP Backend.'); - - $pluginManager = $this->getUserPluginManager(); - $pluginManager->getDisplayName('foo'); - } - - public function testSetDisplayName() { - $pluginManager = $this->getUserPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPUserPluginDummy') - ->setMethods(['respondToActions', 'setDisplayName']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(Backend::SET_DISPLAYNAME); - - $plugin->expects($this->once()) - ->method('setDisplayName') - ->with( - $this->equalTo('user'), - $this->equalTo('password') - ); - - $pluginManager->register($plugin); - $pluginManager->setDisplayName('user', 'password'); - } - - - public function testSetDisplayNameNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements setDisplayName in this LDAP Backend.'); - - $pluginManager = $this->getUserPluginManager(); - $pluginManager->setDisplayName('foo', 'bar'); - } - - public function testCanChangeAvatar() { - $pluginManager = $this->getUserPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPUserPluginDummy') - ->setMethods(['respondToActions', 'canChangeAvatar']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(Backend::PROVIDE_AVATAR); - - $plugin->expects($this->once()) - ->method('canChangeAvatar') - ->with( - $this->equalTo('uid') - ); - - $pluginManager->register($plugin); - $pluginManager->canChangeAvatar('uid'); - } - - - public function testCanChangeAvatarNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements canChangeAvatar in this LDAP Backend.'); - - $pluginManager = $this->getUserPluginManager(); - $pluginManager->canChangeAvatar('foo'); - } - - public function testCountUsers() { - $pluginManager = $this->getUserPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPUserPluginDummy') - ->setMethods(['respondToActions', 'countUsers']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(Backend::COUNT_USERS); - - $plugin->expects($this->once()) - ->method('countUsers'); - - $pluginManager->register($plugin); - $pluginManager->countUsers(); - } - - - public function testCountUsersNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements countUsers in this LDAP Backend.'); - - $pluginManager = $this->getUserPluginManager(); - $pluginManager->countUsers(); - } - - public function testDeleteUser() { - $pluginManager = $this->getUserPluginManager(); - - $plugin = $this->getMockBuilder('OCA\User_LDAP\Tests\LDAPUserPluginDummy') - ->setMethods(['respondToActions', 'canDeleteUser','deleteUser']) - ->getMock(); - - $plugin->expects($this->any()) - ->method('respondToActions') - ->willReturn(0); - - $plugin->expects($this->any()) - ->method('canDeleteUser') - ->willReturn(true); - - $plugin->expects($this->once()) - ->method('deleteUser') - ->with( - $this->equalTo('uid') - ); - - $this->assertFalse($pluginManager->canDeleteUser()); - $pluginManager->register($plugin); - $this->assertTrue($pluginManager->canDeleteUser()); - $pluginManager->deleteUser('uid'); - } - - - public function testDeleteUserNotRegistered() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No plugin implements deleteUser in this LDAP Backend.'); - - $pluginManager = $this->getUserPluginManager(); - $pluginManager->deleteUser('foo'); - } -} diff --git a/apps/user_ldap/tests/User_LDAPTest.php b/apps/user_ldap/tests/User_LDAPTest.php deleted file mode 100644 index 1c10e9955b3fe..0000000000000 --- a/apps/user_ldap/tests/User_LDAPTest.php +++ /dev/null @@ -1,1453 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Jörn Friedrich Dreyer - * @author Lukas Reschke - * @author Morris Jobke - * @author Robin McCorkell - * @author Roeland Jago Douma - * @author Roger Szabo - * @author Thomas Müller - * @author Vinicius Cubas Brand - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests; - -use OC\HintException; -use OC\User\Backend; -use OC\User\Session; -use OCA\User_LDAP\Access; -use OCA\User_LDAP\Connection; -use OCA\User_LDAP\Mapping\AbstractMapping; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\User\Manager; -use OCA\User_LDAP\User\OfflineUser; -use OCA\User_LDAP\User\User; -use OCA\User_LDAP\User_LDAP; -use OCA\User_LDAP\User_LDAP as UserLDAP; -use OCA\User_LDAP\UserPluginManager; -use OCP\IConfig; -use OCP\IUser; -use OCP\Notification\IManager as INotificationManager; -use Test\TestCase; - -/** - * Class Test_User_Ldap_Direct - * - * @group DB - * - * @package OCA\User_LDAP\Tests - */ -class User_LDAPTest extends TestCase { - /** @var User_LDAP */ - protected $backend; - /** @var Access|\PHPUnit\Framework\MockObject\MockObject */ - protected $access; - /** @var OfflineUser|\PHPUnit\Framework\MockObject\MockObject */ - protected $offlineUser; - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - protected $config; - /** @var INotificationManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $notificationManager; - /** @var Session|\PHPUnit\Framework\MockObject\MockObject */ - protected $session; - /** @var UserPluginManager|\PHPUnit\Framework\MockObject\MockObject */ - protected $pluginManager; - /** @var Connection|\PHPUnit\Framework\MockObject\MockObject */ - protected $connection; - /** @var Manager|\PHPUnit\Framework\MockObject\MockObject */ - protected $userManager; - - protected function setUp(): void { - parent::setUp(); - - \OC_User::clearBackends(); - \OC::$server->getGroupManager()->clearBackends(); - - $this->connection = $this->createMock(Connection::class); - $this->userManager = $this->createMock(Manager::class); - - $this->access = $this->createMock(Access::class); - $this->access->connection = $this->connection; - $this->access->userManager = $this->userManager; - - $this->config = $this->createMock(IConfig::class); - $this->notificationManager = $this->createMock(INotificationManager::class); - // Cannot use IUserSession because of private listen() methods - $this->session = $this->createMock(Session::class); - $this->pluginManager = $this->createMock(UserPluginManager::class); - - $this->backend = new User_LDAP( - $this->access, - $this->config, - $this->notificationManager, - $this->session, - $this->pluginManager - ); - } - - private function prepareMockForUserExists() { - $this->access->expects($this->any()) - ->method('username2dn') - ->willReturnCallback(function ($uid) { - switch ($uid) { - case 'gunslinger': - return 'dnOfRoland,dc=test'; - break; - case 'formerUser': - return 'dnOfFormerUser,dc=test'; - break; - case 'newyorker': - return 'dnOfNewYorker,dc=test'; - break; - case 'ladyofshadows': - return 'dnOfLadyOfShadows,dc=test'; - break; - default: - return false; - } - }); - - $this->access->method('fetchUsersByLoginName') - ->willReturn([]); - } - - /** - * Prepares the Access mock for checkPassword tests - * @param bool $noDisplayName - * @return void - */ - private function prepareAccessForCheckPassword($noDisplayName = false) { - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapLoginFilter') { - return '%uid'; - } - return null; - }); - - $this->access->expects($this->any()) - ->method('fetchListOfUsers') - ->willReturnCallback(function ($filter) { - if ($filter === 'roland') { - return [['dn' => ['dnOfRoland,dc=test']]]; - } - return []; - }); - $this->access->expects($this->any()) - ->method('fetchUsersByLoginName') - ->willReturnCallback(function ($uid) { - if ($uid === 'roland') { - return [['dn' => ['dnOfRoland,dc=test']]]; - } - return []; - }); - - $retVal = 'gunslinger'; - if ($noDisplayName === true) { - $retVal = false; - } - $this->access->expects($this->any()) - ->method('dn2username') - ->with($this->equalTo('dnOfRoland,dc=test')) - ->willReturn($retVal); - $this->access->expects($this->any()) - ->method('stringResemblesDN') - ->with($this->equalTo('dnOfRoland,dc=test')) - ->willReturn(true); - $this->access->expects($this->any()) - ->method('areCredentialsValid') - ->willReturnCallback(function ($dn, $pwd) { - if ($pwd === 'dt19') { - return true; - } - return false; - }); - - $this->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['dn', 'uid', 'mail', 'displayname']); - } - - public function testCheckPasswordUidReturn() { - $user = $this->createMock(User::class); - $user->expects($this->any()) - ->method('getUsername') - ->willReturn('gunslinger'); - - $this->prepareAccessForCheckPassword(); - $this->userManager->expects($this->any()) - ->method('get') - ->willReturn($user); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - - \OC_User::useBackend($backend); - - $result = $backend->checkPassword('roland', 'dt19'); - $this->assertEquals('gunslinger', $result); - } - - public function testCheckPasswordWrongPassword() { - $this->prepareAccessForCheckPassword(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $result = $backend->checkPassword('roland', 'wrong'); - $this->assertFalse($result); - } - - public function testCheckPasswordWrongUser() { - $this->prepareAccessForCheckPassword(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $result = $backend->checkPassword('mallory', 'evil'); - $this->assertFalse($result); - } - - public function testCheckPasswordNoDisplayName() { - $this->prepareAccessForCheckPassword(true); - - $this->prepareAccessForCheckPassword(); - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn(null); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $result = $backend->checkPassword('roland', 'dt19'); - $this->assertFalse($result); - } - - public function testCheckPasswordPublicAPI() { - $user = $this->createMock(User::class); - $user->expects($this->any()) - ->method('getUsername') - ->willReturn('gunslinger'); - - $this->prepareAccessForCheckPassword(); - $this->userManager->expects($this->any()) - ->method('get') - ->willReturn($user); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $user = \OC::$server->getUserManager()->checkPassword('roland', 'dt19'); - $result = false; - if ($user !== false) { - $result = $user->getUID(); - } - $this->assertEquals('gunslinger', $result); - } - - public function testCheckPasswordPublicAPIWrongPassword() { - $this->prepareAccessForCheckPassword(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $user = \OC::$server->getUserManager()->checkPassword('roland', 'wrong'); - $result = false; - if ($user !== false) { - $result = $user->getUID(); - } - $this->assertFalse($result); - } - - public function testCheckPasswordPublicAPIWrongUser() { - $this->prepareAccessForCheckPassword(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $user = \OC::$server->getUserManager()->checkPassword('mallory', 'evil'); - $result = false; - if ($user !== false) { - $result = $user->getUID(); - } - $this->assertFalse($result); - } - - public function testDeleteUserCancel() { - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $result = $backend->deleteUser('notme'); - $this->assertFalse($result); - } - - public function testDeleteUserSuccess() { - $uid = 'jeremy'; - $home = '/var/vhome/jdings/'; - - $mapping = $this->createMock(UserMapping::class); - $mapping->expects($this->once()) - ->method('unmap') - ->willReturn(true); - $this->access->expects($this->once()) - ->method('getUserMapper') - ->willReturn($mapping); - $this->connection->expects($this->any()) - ->method('getConnectionResource') - ->willReturn('this is an ldap link'); - - $this->config->expects($this->any()) - ->method('getUserValue') - ->with($uid, 'user_ldap', 'isDeleted') - ->willReturn('1'); - - $offlineUser = $this->createMock(OfflineUser::class); - $offlineUser->expects($this->once()) - ->method('getHomePath') - ->willReturn($home); - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($offlineUser); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - - $result = $backend->deleteUser($uid); - $this->assertTrue($result); - /** @noinspection PhpUnhandledExceptionInspection */ - $this->assertSame($backend->getHome($uid), $home); - } - - public function testDeleteUserWithPlugin() { - $this->pluginManager->expects($this->once()) - ->method('canDeleteUser') - ->willReturn(true); - $this->pluginManager->expects($this->once()) - ->method('deleteUser') - ->with('uid') - ->willReturn(true); - - $this->config->expects($this->once()) - ->method('getUserValue') - ->with('uid', 'user_ldap', 'isDeleted', 0) - ->willReturn(1); - - $mapper = $this->createMock(UserMapping::class); - $mapper->expects($this->once()) - ->method('unmap') - ->with('uid'); - - $this->access->expects($this->atLeastOnce()) - ->method('getUserMapper') - ->willReturn($mapper); - - $this->userManager->expects($this->once()) - ->method('invalidate') - ->with('uid'); - - $this->assertEquals(true, $this->backend->deleteUser('uid')); - } - - /** - * Prepares the Access mock for getUsers tests - */ - private function prepareAccessForGetUsers() { - $this->access->expects($this->once()) - ->method('escapeFilterPart') - ->willReturnCallback(function ($search) { - return $search; - }); - $this->access->expects($this->any()) - ->method('getFilterPartForUserSearch') - ->willReturnCallback(function ($search) { - return $search; - }); - $this->access->expects($this->any()) - ->method('combineFilterWithAnd') - ->willReturnCallback(function ($param) { - return $param[2]; - }); - $this->access->expects($this->any()) - ->method('fetchListOfUsers') - ->willReturnCallback(function ($search, $a, $l, $o) { - $users = ['gunslinger', 'newyorker', 'ladyofshadows']; - if (empty($search)) { - $result = $users; - } else { - $result = []; - foreach ($users as $user) { - if (stripos($user, $search) !== false) { - $result[] = $user; - } - } - } - if (!is_null($l) || !is_null($o)) { - $result = array_slice($result, $o, $l); - } - return $result; - }); - $this->access->expects($this->any()) - ->method('nextcloudUserNames') - ->willReturnArgument(0); - $this->access->method('fetchUsersByLoginName') - ->willReturn([]); - - $this->access->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['dn', 'uid', 'mail', 'displayname']); - } - - public function testGetUsersNoParam() { - $this->prepareAccessForGetUsers(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - - $result = $backend->getUsers(); - $this->assertEquals(3, count($result)); - } - - public function testGetUsersLimitOffset() { - $this->prepareAccessForGetUsers(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - - $result = $backend->getUsers('', 1, 2); - $this->assertEquals(1, count($result)); - } - - public function testGetUsersLimitOffset2() { - $this->prepareAccessForGetUsers(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - - $result = $backend->getUsers('', 2, 1); - $this->assertEquals(2, count($result)); - } - - public function testGetUsersSearchWithResult() { - $this->prepareAccessForGetUsers(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - - $result = $backend->getUsers('yo'); - $this->assertEquals(2, count($result)); - } - - public function testGetUsersSearchEmptyResult() { - $this->prepareAccessForGetUsers(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - - $result = $backend->getUsers('nix'); - $this->assertEquals(0, count($result)); - } - - private function getUsers($search = '', $limit = null, $offset = null) { - $users = \OC::$server->getUserManager()->search($search, $limit, $offset); - $uids = array_map(function (IUser $user) { - return $user->getUID(); - }, $users); - return $uids; - } - - public function testGetUsersViaAPINoParam() { - $this->prepareAccessForGetUsers(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $result = $this->getUsers(); - $this->assertEquals(3, count($result)); - } - - public function testGetUsersViaAPILimitOffset() { - $this->prepareAccessForGetUsers(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $result = $this->getUsers('', 1, 2); - $this->assertEquals(1, count($result)); - } - - public function testGetUsersViaAPILimitOffset2() { - $this->prepareAccessForGetUsers(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $result = $this->getUsers('', 2, 1); - $this->assertEquals(2, count($result)); - } - - public function testGetUsersViaAPISearchWithResult() { - $this->prepareAccessForGetUsers(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $result = $this->getUsers('yo'); - $this->assertEquals(2, count($result)); - } - - public function testGetUsersViaAPISearchEmptyResult() { - $this->prepareAccessForGetUsers(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $result = $this->getUsers('nix'); - $this->assertEquals(0, count($result)); - } - - public function testUserExists() { - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->prepareMockForUserExists(); - - $user = $this->createMock(User::class); - - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($user); - $this->access->expects($this->any()) - ->method('getUserMapper') - ->willReturn($this->createMock(UserMapping::class)); - - //test for existing user - /** @noinspection PhpUnhandledExceptionInspection */ - $result = $backend->userExists('gunslinger'); - $this->assertTrue($result); - } - - public function testUserExistsForDeleted() { - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->prepareMockForUserExists(); - - $mapper = $this->createMock(UserMapping::class); - $mapper->expects($this->any()) - ->method('getUUIDByDN') - ->with('dnOfFormerUser,dc=test') - ->willReturn('45673458748'); - - $this->access->expects($this->any()) - ->method('getUserMapper') - ->willReturn($mapper); - - $user = $this->createMock(User::class); - - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($user); - - //test for deleted user – always returns true as long as we have the user in DB - $this->assertTrue($backend->userExists('formerUser')); - } - - public function testUserExistsForNeverExisting() { - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->prepareMockForUserExists(); - - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn) { - if ($dn === 'dnOfRoland,dc=test') { - return []; - } - return false; - }); - - //test for never-existing user - /** @noinspection PhpUnhandledExceptionInspection */ - $result = $backend->userExists('mallory'); - $this->assertFalse($result); - } - - public function testUserExistsPublicAPI() { - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->prepareMockForUserExists(); - \OC_User::useBackend($backend); - - $user = $this->createMock(User::class); - $user->expects($this->any()) - ->method('getDN') - ->willReturn('dnOfRoland,dc=test'); - - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn) { - if ($dn === 'dnOfRoland,dc=test') { - return []; - } - return false; - }); - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($user); - $this->access->expects($this->any()) - ->method('getUserMapper') - ->willReturn($this->createMock(UserMapping::class)); - - //test for existing user - $result = \OC::$server->getUserManager()->userExists('gunslinger'); - $this->assertTrue($result); - } - - public function testDeleteUserExisting() { - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - - //we do not support deleting existing users at all - $result = $backend->deleteUser('gunslinger'); - $this->assertFalse($result); - } - - public function testGetHomeAbsolutePath() { - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->prepareMockForUserExists(); - - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'homeFolderNamingRule') { - return 'attr:testAttribute'; - } - return null; - }); - - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - switch ($dn) { - case 'dnOfRoland,dc=test': - if ($attr === 'testAttribute') { - return ['/tmp/rolandshome/']; - } - return []; - break; - default: - return false; - } - }); - - $user = $this->createMock(User::class); - $user->expects($this->any()) - ->method('getUsername') - ->willReturn('gunslinger'); - $user->expects($this->any()) - ->method('getDN') - ->willReturn('dnOfRoland,dc=test'); - $user->expects($this->any()) - ->method('getHomePath') - ->willReturn('/tmp/rolandshome/'); - - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($user); - - //absolute path - /** @noinspection PhpUnhandledExceptionInspection */ - $result = $backend->getHome('gunslinger'); - $this->assertEquals('/tmp/rolandshome/', $result); - } - - public function testGetHomeRelative() { - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->prepareMockForUserExists(); - - $dataDir = \OC::$server->getConfig()->getSystemValue( - 'datadirectory', \OC::$SERVERROOT.'/data'); - - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'homeFolderNamingRule') { - return 'attr:testAttribute'; - } - return null; - }); - - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - switch ($dn) { - case 'dnOfLadyOfShadows,dc=test': - if ($attr === 'testAttribute') { - return ['susannah/']; - } - return []; - break; - default: - return false; - } - }); - - $user = $this->createMock(User::class); - $user->expects($this->any()) - ->method('getUsername') - ->willReturn('ladyofshadows'); - $user->expects($this->any()) - ->method('getDN') - ->willReturn('dnOfLadyOfShadows,dc=test'); - $user->expects($this->any()) - ->method('getHomePath') - ->willReturn($dataDir.'/susannah/'); - - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($user); - - /** @noinspection PhpUnhandledExceptionInspection */ - $result = $backend->getHome('ladyofshadows'); - $this->assertEquals($dataDir.'/susannah/', $result); - } - - - public function testGetHomeNoPath() { - $this->expectException(\Exception::class); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->prepareMockForUserExists(); - - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'homeFolderNamingRule') { - return 'attr:testAttribute'; - } - return null; - }); - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - switch ($dn) { - default: - return false; - } - }); - $this->access->connection->expects($this->any()) - ->method('getFromCache') - ->willReturnCallback(function ($key) { - if ($key === 'userExistsnewyorker') { - return true; - } - return null; - }); - - $user = $this->createMock(User::class); - $user->expects($this->any()) - ->method('getUsername') - ->willReturn('newyorker'); - $user->expects($this->any()) - ->method('getHomePath') - ->willThrowException(new \Exception()); - - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($user); - - //no path at all – triggers OC default behaviour - $result = $backend->getHome('newyorker'); - $this->assertFalse($result); - } - - public function testGetHomeDeletedUser() { - $uid = 'newyorker'; - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->prepareMockForUserExists(); - - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'homeFolderNamingRule') { - return 'attr:testAttribute'; - } - return null; - }); - - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturn([]); - - $userMapper = $this->createMock(UserMapping::class); - - $this->access->expects($this->any()) - ->method('getUserMapper') - ->willReturn($userMapper); - - $this->config->expects($this->any()) - ->method('getUserValue') - ->willReturn(true); - - $offlineUser = $this->createMock(OfflineUser::class); - $offlineUser->expects($this->atLeastOnce()) - ->method('getHomePath') - ->willReturn(''); - - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($offlineUser); - - $result = $backend->getHome($uid); - $this->assertFalse($result); - } - - public function testGetHomeWithPlugin() { - $this->pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(Backend::GET_HOME) - ->willReturn(true); - $this->pluginManager->expects($this->once()) - ->method('getHome') - ->with('uid') - ->willReturn('result'); - - $this->connection->expects($this->any()) - ->method('getFromCache') - ->willReturnCallback(function ($uid) { - return true; - }); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->assertEquals($this->backend->getHome('uid'),'result'); - } - - private function prepareAccessForGetDisplayName() { - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapUserDisplayName') { - return 'displayname'; - } - return null; - }); - - $this->access->expects($this->any()) - ->method('readAttribute') - ->willReturnCallback(function ($dn, $attr) { - switch ($dn) { - case 'dnOfRoland,dc=test': - if ($attr === 'displayname') { - return ['Roland Deschain']; - } - return []; - break; - - default: - return false; - } - }); - $this->access->method('fetchUsersByLoginName') - ->willReturn([]); - } - - public function testGetDisplayName() { - $this->prepareAccessForGetDisplayName(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->prepareMockForUserExists(); - - $this->connection->expects($this->any()) - ->method('getConnectionResource') - ->willReturnCallback(function () { - return true; - }); - - $user1 = $this->createMock(User::class); - $user1->expects($this->once()) - ->method('composeAndStoreDisplayName') - ->willReturn('Roland Deschain'); - $user1->expects($this->any()) - ->method('getDN') - ->willReturn('dnOfRoland,dc=test'); - - $user2 = $this->createMock(User::class); - $user2->expects($this->never()) - ->method('composeAndStoreDisplayName'); - $user2->expects($this->any()) - ->method('getDN') - ->willReturn('another DN'); - - $mapper = $this->createMock(UserMapping::class); - $mapper->expects($this->any()) - ->method('getUUIDByDN') - ->willReturnCallback(function ($dn) { - return $dn; - }); - - $this->userManager->expects($this->any()) - ->method('get') - ->willReturnCallback(function ($uid) use ($user1, $user2) { - if ($uid === 'gunslinger') { - return $user1; - } elseif ($uid === 'newyorker') { - return $user2; - } - return null; - }); - $this->access->expects($this->any()) - ->method('getUserMapper') - ->willReturn($mapper); - $this->access->expects($this->any()) - ->method('getUserDnByUuid') - ->willReturnCallback(function ($uuid) { - return $uuid . '1'; - }); - - //with displayName - $result = $backend->getDisplayName('gunslinger'); - $this->assertEquals('Roland Deschain', $result); - - //empty displayname retrieved - $result = $backend->getDisplayName('newyorker'); - $this->assertEquals(null, $result); - } - - public function testGetDisplayNamePublicAPI() { - $this->access->expects($this->any()) - ->method('username2dn') - ->willReturnCallback(function ($uid) { - switch ($uid) { - case 'gunslinger': - return 'dnOfRoland,dc=test'; - break; - case 'formerUser': - return 'dnOfFormerUser,dc=test'; - break; - case 'newyorker': - return 'dnOfNewYorker,dc=test'; - break; - case 'ladyofshadows': - return 'dnOfLadyOfShadows,dc=test'; - break; - default: - return false; - } - }); - $this->prepareAccessForGetDisplayName(); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->prepareMockForUserExists(); - - $this->connection->expects($this->any()) - ->method('getConnectionResource') - ->willReturnCallback(function () { - return true; - }); - - \OC_User::useBackend($backend); - - $user1 = $this->createMock(User::class); - $user1->expects($this->once()) - ->method('composeAndStoreDisplayName') - ->willReturn('Roland Deschain'); - $user1->expects($this->any()) - ->method('getDN') - ->willReturn('dnOfRoland,dc=test'); - - $user2 = $this->createMock(User::class); - $user2->expects($this->never()) - ->method('composeAndStoreDisplayName'); - $user2->expects($this->any()) - ->method('getDN') - ->willReturn('another DN'); - - $mapper = $this->createMock(UserMapping::class); - $mapper->expects($this->any()) - ->method('getUUIDByDN') - ->willReturnCallback(function ($dn) { - return $dn; - }); - - $this->userManager->expects($this->any()) - ->method('get') - ->willReturnCallback(function ($uid) use ($user1, $user2) { - if ($uid === 'gunslinger') { - return $user1; - } elseif ($uid === 'newyorker') { - return $user2; - } - return null; - }); - $this->access->expects($this->any()) - ->method('getUserMapper') - ->willReturn($mapper); - $this->access->expects($this->any()) - ->method('getUserDnByUuid') - ->willReturnCallback(function ($uuid) { - return $uuid . '1'; - }); - - //with displayName - $result = \OC::$server->getUserManager()->get('gunslinger')->getDisplayName(); - $this->assertEquals('Roland Deschain', $result); - - //empty displayname retrieved - $result = \OC::$server->getUserManager()->get('newyorker') === null ? 'newyorker' : \OC::$server->getUserManager()->get('newyorker')->getDisplayName(); - $this->assertEquals('newyorker', $result); - } - - public function testGetDisplayNameWithPlugin() { - $this->pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(Backend::GET_DISPLAYNAME) - ->willReturn(true); - $this->pluginManager->expects($this->once()) - ->method('getDisplayName') - ->with('uid') - ->willReturn('result'); - - $this->assertEquals($this->backend->getDisplayName('uid'),'result'); - } - - //no test for getDisplayNames, because it just invokes getUsers and - //getDisplayName - - public function testCountUsers() { - $this->access->expects($this->once()) - ->method('countUsers') - ->willReturn(5); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - - $result = $backend->countUsers(); - $this->assertEquals(5, $result); - } - - public function testCountUsersFailing() { - $this->access->expects($this->once()) - ->method('countUsers') - ->willReturn(false); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - - $result = $backend->countUsers(); - $this->assertFalse($result); - } - - public function testCountUsersWithPlugin() { - $this->pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(Backend::COUNT_USERS) - ->willReturn(true); - $this->pluginManager->expects($this->once()) - ->method('countUsers') - ->willReturn(42); - - $this->assertEquals($this->backend->countUsers(),42); - } - - public function testLoginName2UserNameSuccess() { - $loginName = 'Alice'; - $username = 'alice'; - $dn = 'uid=alice,dc=what,dc=ever'; - - $this->access->expects($this->once()) - ->method('fetchUsersByLoginName') - ->with($this->equalTo($loginName)) - ->willReturn([['dn' => [$dn]]]); - $this->access->expects($this->any()) - ->method('stringResemblesDN') - ->with($this->equalTo($dn)) - ->willReturn(true); - $this->access->expects($this->any()) - ->method('dn2username') - ->with($this->equalTo($dn)) - ->willReturn($username); - - $this->connection->expects($this->exactly(2)) - ->method('getFromCache') - ->with($this->equalTo('loginName2UserName-'.$loginName)) - ->willReturnOnConsecutiveCalls(null, $username); - $this->connection->expects($this->once()) - ->method('writeToCache') - ->with($this->equalTo('loginName2UserName-'.$loginName), $this->equalTo($username)); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $user = $this->createMock(User::class); - $user->expects($this->any()) - ->method('getUsername') - ->willReturn('alice'); - - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->with($dn) - ->willReturn($user); - $this->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['dn', 'uid', 'mail', 'displayname']); - - $name = $backend->loginName2UserName($loginName); - $this->assertSame($username, $name); - - // and once again to verify that caching works - $backend->loginName2UserName($loginName); - } - - public function testLoginName2UserNameNoUsersOnLDAP() { - $loginName = 'Loki'; - - $this->access->expects($this->once()) - ->method('fetchUsersByLoginName') - ->with($this->equalTo($loginName)) - ->willReturn([]); - $this->access->expects($this->never()) - ->method('stringResemblesDN'); - $this->access->expects($this->never()) - ->method('dn2username'); - - $this->connection->expects($this->exactly(2)) - ->method('getFromCache') - ->with($this->equalTo('loginName2UserName-'.$loginName)) - ->willReturnOnConsecutiveCalls(null, false); - $this->connection->expects($this->once()) - ->method('writeToCache') - ->with($this->equalTo('loginName2UserName-'.$loginName), false); - - $this->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['dn', 'uid', 'mail', 'displayname']); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $name = $backend->loginName2UserName($loginName); - $this->assertSame(false, $name); - - // and once again to verify that caching works - $backend->loginName2UserName($loginName); - } - - public function testLoginName2UserNameOfflineUser() { - $loginName = 'Alice'; - $dn = 'uid=alice,dc=what,dc=ever'; - - $offlineUser = $this->getMockBuilder(OfflineUser::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->access->expects($this->once()) - ->method('fetchUsersByLoginName') - ->with($this->equalTo($loginName)) - ->willReturn([['dn' => [$dn]]]); - - $this->connection->expects($this->exactly(2)) - ->method('getFromCache') - ->with($this->equalTo('loginName2UserName-'.$loginName)) - ->willReturnOnConsecutiveCalls(null, false); - $this->connection->expects($this->once()) - ->method('writeToCache') - ->with($this->equalTo('loginName2UserName-'.$loginName), $this->equalTo(false)); - - $this->userManager->expects($this->any()) - ->method('get') - ->with($dn) - ->willReturn($offlineUser); - $this->userManager->expects($this->any()) - ->method('getAttributes') - ->willReturn(['dn', 'uid', 'mail', 'displayname']); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $name = $backend->loginName2UserName($loginName); - $this->assertSame(false, $name); - - // and once again to verify that caching works - $backend->loginName2UserName($loginName); - } - - /** - * Prepares the Access mock for setPassword tests - * - * @param bool $enablePasswordChange - */ - private function prepareAccessForSetPassword($enablePasswordChange = true) { - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) use (&$enablePasswordChange) { - if ($name === 'ldapLoginFilter') { - return '%uid'; - } - if ($name === 'turnOnPasswordChange') { - return $enablePasswordChange?1:0; - } - return null; - }); - $this->connection->expects($this->any()) - ->method('getFromCache') - ->willReturnCallback(function ($uid) { - if ($uid === 'userExists'.'roland') { - return true; - } - return null; - }); - - $this->access->expects($this->any()) - ->method('fetchListOfUsers') - ->willReturnCallback(function ($filter) { - if ($filter === 'roland') { - return [['dn' => ['dnOfRoland,dc=test']]]; - } - return []; - }); - $this->access->expects($this->any()) - ->method('fetchUsersByLoginName') - ->willReturnCallback(function ($uid) { - if ($uid === 'roland') { - return [['dn' => ['dnOfRoland,dc=test']]]; - } - return []; - }); - $this->access->expects($this->any()) - ->method('dn2username') - ->with($this->equalTo('dnOfRoland,dc=test')) - ->willReturn('roland'); - $this->access->expects($this->any()) - ->method('stringResemblesDN') - ->with($this->equalTo('dnOfRoland,dc=test')) - ->willReturn(true); - $this->access->expects($this->any()) - ->method('setPassword') - ->willReturnCallback(function ($uid, $password) { - if (strlen($password) <= 5) { - throw new HintException('Password fails quality checking policy', '', 19); - } - return true; - }); - } - - - public function testSetPasswordInvalid() { - $this->expectException(\OC\HintException::class); - $this->expectExceptionMessage('Password fails quality checking policy'); - - $this->prepareAccessForSetPassword($this->access); - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($this->createMock(User::class)); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $this->assertTrue(\OC_User::setPassword('roland', 'dt')); - } - - public function testSetPasswordValid() { - $this->prepareAccessForSetPassword($this->access); - - $this->userManager->expects($this->any()) - ->method('get') - ->willReturn($this->createMock(User::class)); - - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - $this->userManager->expects($this->any()) - ->method('get') - ->willReturn($this->createMock(User::class)); - - \OC_User::useBackend($backend); - - $this->assertTrue(\OC_User::setPassword('roland', 'dt12234$')); - } - - public function testSetPasswordValidDisabled() { - $this->userManager->expects($this->any()) - ->method('get') - ->willReturn($this->createMock(User::class)); - - $this->prepareAccessForSetPassword(false); - $backend = new UserLDAP($this->access, $this->config, $this->notificationManager, $this->session, $this->pluginManager); - \OC_User::useBackend($backend); - - $this->assertFalse(\OC_User::setPassword('roland', 'dt12234$')); - } - - - public function testSetPasswordWithInvalidUser() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('LDAP setPassword: Could not get user object for uid NotExistingUser. Maybe the LDAP entry has no set display name attribute?'); - - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('NotExistingUser') - ->willReturn(null); - - $this->backend->setPassword('NotExistingUser', 'Password'); - } - - public function testSetPasswordWithUsernameFalse() { - $user = $this->createMock(User::class); - $user - ->expects($this->once()) - ->method('getUsername') - ->willReturn(false); - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('NotExistingUser') - ->willReturn($user); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->assertFalse($this->backend->setPassword('NotExistingUser', 'Password')); - } - - public function testSetPasswordWithPlugin() { - $this->pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(Backend::SET_PASSWORD) - ->willReturn(true); - $this->pluginManager->expects($this->once()) - ->method('setPassword') - ->with('uid','password') - ->willReturn('result'); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->assertEquals($this->backend->setPassword('uid', 'password'),'result'); - } - - public function avatarDataProvider() { - return [ - [ 'validImageData', false ], - [ 'corruptImageData', true ], - [ false, true] - ]; - } - - /** @dataProvider avatarDataProvider */ - public function testCanChangeAvatar($imageData, $expected) { - $isValidImage = strpos((string)$imageData, 'valid') === 0; - - $user = $this->createMock(User::class); - $user->expects($this->once()) - ->method('getAvatarImage') - ->willReturn($imageData); - $user->expects($this->atMost(1)) - ->method('updateAvatar') - ->willReturn($isValidImage); - - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($user); - - /** @noinspection PhpUnhandledExceptionInspection */ - $this->assertSame($expected, $this->backend->canChangeAvatar('uid')); - } - - public function testCanChangeAvatarWithPlugin() { - $this->pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(Backend::PROVIDE_AVATAR) - ->willReturn(true); - $this->pluginManager->expects($this->once()) - ->method('canChangeAvatar') - ->with('uid') - ->willReturn('result'); - - $this->assertEquals($this->backend->canChangeAvatar('uid'),'result'); - } - - public function testSetDisplayNameWithPlugin() { - $newDisplayName = 'J. Baker'; - $this->pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(Backend::SET_DISPLAYNAME) - ->willReturn(true); - $this->pluginManager->expects($this->once()) - ->method('setDisplayName') - ->with('uid', $newDisplayName) - ->willReturn($newDisplayName); - $this->access->expects($this->once()) - ->method('cacheUserDisplayName'); - - $this->assertEquals($newDisplayName, $this->backend->setDisplayName('uid', $newDisplayName)); - } - - - public function testSetDisplayNameErrorWithPlugin() { - $this->expectException(\OC\HintException::class); - - $newDisplayName = 'J. Baker'; - $this->pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(Backend::SET_DISPLAYNAME) - ->willReturn(true); - $this->pluginManager->expects($this->once()) - ->method('setDisplayName') - ->with('uid', $newDisplayName) - ->willThrowException(new HintException('something happned')); - $this->access->expects($this->never()) - ->method('cacheUserDisplayName'); - - $this->backend->setDisplayName('uid', $newDisplayName); - } - - public function testSetDisplayNameFailing() { - $this->pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(Backend::SET_DISPLAYNAME) - ->willReturn(false); - $this->access->expects($this->never()) - ->method('cacheUserDisplayName'); - - $this->assertFalse($this->backend->setDisplayName('uid', 'displayName')); - } - - public function testCreateUserWithPlugin() { - $uid = 'alien6372'; - $uuid = '123-2345-36756-123-2345234-4431'; - $pwd = 'passwørd'; - - $this->pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(Backend::CREATE_USER) - ->willReturn(true); - $this->pluginManager->expects($this->once()) - ->method('createUser') - ->with($uid, $pwd) - ->willReturn('result'); - - $this->access->expects($this->atLeastOnce()) - ->method('getUUID') - ->willReturn($uuid); - $this->access->expects($this->once()) - ->method('mapAndAnnounceIfApplicable') - ->with($this->isInstanceOf(AbstractMapping::class), $this->anything(), $uid, $uuid, true); - $this->access->expects($this->any()) - ->method('getUserMapper') - ->willReturn($this->createMock(AbstractMapping::class)); - - $this->assertEquals($this->backend->createUser($uid, $pwd),true); - } - - public function testCreateUserFailing() { - $this->pluginManager->expects($this->once()) - ->method('implementsActions') - ->with(Backend::CREATE_USER) - ->willReturn(false); - - $this->assertFalse($this->backend->createUser('uid', 'password')); - } - - public function actionProvider() { - return [ - [ 'ldapUserAvatarRule', 'default', Backend::PROVIDE_AVATAR, true] , - [ 'ldapUserAvatarRule', 'data:selfiePhoto', Backend::PROVIDE_AVATAR, true], - [ 'ldapUserAvatarRule', 'none', Backend::PROVIDE_AVATAR, false], - [ 'turnOnPasswordChange', 0, Backend::SET_PASSWORD, false], - [ 'turnOnPasswordChange', 1, Backend::SET_PASSWORD, true], - ]; - } - - /** - * @dataProvider actionProvider - */ - public function testImplementsAction($configurable, $value, $actionCode, $expected) { - $this->pluginManager->expects($this->once()) - ->method('getImplementedActions') - ->willReturn(0); - - $this->connection->expects($this->any()) - ->method('__get') - ->willReturnMap([ - [$configurable, $value], - ]); - - $this->assertSame($expected, $this->backend->implementsActions($actionCode)); - } -} diff --git a/apps/user_ldap/tests/User_ProxyTest.php b/apps/user_ldap/tests/User_ProxyTest.php deleted file mode 100644 index 5753990a73608..0000000000000 --- a/apps/user_ldap/tests/User_ProxyTest.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * @author Arthur Schiwon - * @author Christoph Wurst - * @author Lukas Reschke - * @author Morris Jobke - * @author Roeland Jago Douma - * @author Roger Szabo - * @author Vinicius Cubas Brand - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\ILDAPWrapper; -use OCA\User_LDAP\User_Proxy; -use OCA\User_LDAP\UserPluginManager; -use OCP\IConfig; -use OCP\IUserSession; -use OCP\Notification\IManager as INotificationManager; -use Test\TestCase; - -class User_ProxyTest extends TestCase { - /** @var ILDAPWrapper|\PHPUnit\Framework\MockObject\MockObject */ - private $ldapWrapper; - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - private $config; - /** @var INotificationManager|\PHPUnit\Framework\MockObject\MockObject */ - private $notificationManager; - /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ - private $userSession; - /** @var User_Proxy|\PHPUnit\Framework\MockObject\MockObject */ - private $proxy; - /** @var UserPluginManager|\PHPUnit\Framework\MockObject\MockObject */ - private $userPluginManager; - - protected function setUp(): void { - parent::setUp(); - - $this->ldapWrapper = $this->createMock(ILDAPWrapper::class); - $this->config = $this->createMock(IConfig::class); - $this->notificationManager = $this->createMock(INotificationManager::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->userPluginManager = $this->createMock(UserPluginManager::class); - $this->proxy = $this->getMockBuilder(User_Proxy::class) - ->setConstructorArgs([ - [], - $this->ldapWrapper, - $this->config, - $this->notificationManager, - $this->userSession, - $this->userPluginManager - ]) - ->setMethods(['handleRequest']) - ->getMock(); - } - - public function testSetPassword() { - $this->proxy - ->expects($this->once()) - ->method('handleRequest') - ->with('MyUid', 'setPassword', ['MyUid', 'MyPassword']) - ->willReturn(true); - - $this->assertTrue($this->proxy->setPassword('MyUid', 'MyPassword')); - } - - public function testSetDisplayName() { - $this->proxy - ->expects($this->once()) - ->method('handleRequest') - ->with('MyUid', 'setDisplayName', ['MyUid', 'MyPassword']) - ->willReturn(true); - - $this->assertTrue($this->proxy->setDisplayName('MyUid', 'MyPassword')); - } - - public function testCreateUser() { - $this->proxy - ->expects($this->once()) - ->method('handleRequest') - ->with('MyUid', 'createUser', ['MyUid', 'MyPassword']) - ->willReturn(true); - - $this->assertTrue($this->proxy->createUser('MyUid', 'MyPassword')); - } -} diff --git a/apps/user_ldap/tests/WizardTest.php b/apps/user_ldap/tests/WizardTest.php deleted file mode 100644 index da3239cb8f8d2..0000000000000 --- a/apps/user_ldap/tests/WizardTest.php +++ /dev/null @@ -1,451 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Morris Jobke - * @author Roeland Jago Douma - * @author Thomas Müller - * @author Victor Dubiniuk - * @author Viktor Szépe - * - * @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 - * - */ - -namespace OCA\User_LDAP\Tests; - -use OCA\User_LDAP\Access; -use OCA\User_LDAP\Configuration; -use OCA\User_LDAP\ILDAPWrapper; -use OCA\User_LDAP\Wizard; -use PHPUnit\Framework\MockObject\MockObject; -use Test\TestCase; - -/** - * Class Test_Wizard - * - * @group DB - * - * @package OCA\User_LDAP\Tests - */ -class WizardTest extends TestCase { - protected function setUp(): void { - parent::setUp(); - //we need to make sure the consts are defined, otherwise tests will fail - //on systems without php5_ldap - $ldapConsts = ['LDAP_OPT_PROTOCOL_VERSION', - 'LDAP_OPT_REFERRALS', 'LDAP_OPT_NETWORK_TIMEOUT']; - foreach ($ldapConsts as $const) { - if (!defined($const)) { - define($const, 42); - } - } - } - - private function getWizardAndMocks() { - static $confMethods; - static $connMethods; - static $accMethods; - - if (is_null($confMethods)) { - $confMethods = get_class_methods('\OCA\User_LDAP\Configuration'); - $connMethods = get_class_methods('\OCA\User_LDAP\Connection'); - $accMethods = get_class_methods('\OCA\User_LDAP\Access'); - } - /** @var ILDAPWrapper|\PHPUnit\Framework\MockObject\MockObject $lw */ - $lw = $this->createMock(ILDAPWrapper::class); - - /** @var Configuration|\PHPUnit\Framework\MockObject\MockObject $conf */ - $conf = $this->getMockBuilder(Configuration::class) - ->setMethods($confMethods) - ->setConstructorArgs([$lw, null, null]) - ->getMock(); - - /** @var Access|\PHPUnit\Framework\MockObject\MockObject $access */ - $access = $this->createMock(Access::class); - - return [new Wizard($conf, $lw, $access), $conf, $lw, $access]; - } - - private function prepareLdapWrapperForConnections(MockObject &$ldap) { - $ldap->expects($this->once()) - ->method('connect') - //dummy value, usually invalid - ->willReturn(true); - - $ldap->expects($this->exactly(3)) - ->method('setOption') - ->willReturn(true); - - $ldap->expects($this->once()) - ->method('bind') - ->willReturn(true); - } - - public function testCumulativeSearchOnAttributeLimited() { - list($wizard, $configuration, $ldap) = $this->getWizardAndMocks(); - - $configuration->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapBase') { - return ['base']; - } - return null; - }); - - $this->prepareLdapWrapperForConnections($ldap); - - $ldap->expects($this->any()) - ->method('isResource') - ->willReturn(true); - - $ldap->expects($this->exactly(2)) - ->method('search') - //dummy value, usually invalid - ->willReturn(true); - - $ldap->expects($this->exactly(2)) - ->method('countEntries') - //an is_resource check will follow, so we need to return a dummy resource - ->willReturn(23); - - //5 DNs per filter means 2x firstEntry and 8x nextEntry - $ldap->expects($this->exactly(2)) - ->method('firstEntry') - //dummy value, usually invalid - ->willReturn(true); - - $ldap->expects($this->exactly(8)) - ->method('nextEntry') - //dummy value, usually invalid - ->willReturn(true); - - $ldap->expects($this->exactly(10)) - ->method('getAttributes') - //dummy value, usually invalid - ->willReturn(['cn' => ['foo'], 'count' => 1]); - - global $uidnumber; - $uidnumber = 1; - $ldap->expects($this->exactly(10)) - ->method('getDN') - //dummy value, usually invalid - ->willReturnCallback(function ($a, $b) { - global $uidnumber; - return $uidnumber++; - }); - - // The following expectations are the real test - $filters = ['f1', 'f2', '*']; - $wizard->cumulativeSearchOnAttribute($filters, 'cn', 5); - unset($uidnumber); - } - - public function testCumulativeSearchOnAttributeUnlimited() { - list($wizard, $configuration, $ldap) = $this->getWizardAndMocks(); - - $configuration->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapBase') { - return ['base']; - } - return null; - }); - - $this->prepareLdapWrapperForConnections($ldap); - - $ldap->expects($this->any()) - ->method('isResource') - ->willReturnCallback(function ($r) { - if ($r === true) { - return true; - } - if ($r % 24 === 0) { - global $uidnumber; - $uidnumber++; - return false; - } - return true; - }); - - $ldap->expects($this->exactly(2)) - ->method('search') - //dummy value, usually invalid - ->willReturn(true); - - $ldap->expects($this->exactly(2)) - ->method('countEntries') - //an is_resource check will follow, so we need to return a dummy resource - ->willReturn(23); - - //5 DNs per filter means 2x firstEntry and 8x nextEntry - $ldap->expects($this->exactly(2)) - ->method('firstEntry') - //dummy value, usually invalid - ->willReturnCallback(function ($r) { - global $uidnumber; - return $uidnumber; - }); - - $ldap->expects($this->exactly(46)) - ->method('nextEntry') - //dummy value, usually invalid - ->willReturnCallback(function ($r) { - global $uidnumber; - return $uidnumber; - }); - - $ldap->expects($this->exactly(46)) - ->method('getAttributes') - //dummy value, usually invalid - ->willReturn(['cn' => ['foo'], 'count' => 1]); - - global $uidnumber; - $uidnumber = 1; - $ldap->expects($this->exactly(46)) - ->method('getDN') - //dummy value, usually invalid - ->willReturnCallback(function ($a, $b) { - global $uidnumber; - return $uidnumber++; - }); - - // The following expectations are the real test - $filters = ['f1', 'f2', '*']; - $wizard->cumulativeSearchOnAttribute($filters, 'cn', 0); - unset($uidnumber); - } - - public function testDetectEmailAttributeAlreadySet() { - list($wizard, $configuration, $ldap, $access) - = $this->getWizardAndMocks(); - - $configuration->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapEmailAttribute') { - return 'myEmailAttribute'; - } else { - //for requirement checks - return 'let me pass'; - } - }); - - $access->expects($this->once()) - ->method('countUsers') - ->willReturn(42); - - $wizard->detectEmailAttribute(); - } - - public function testDetectEmailAttributeOverrideSet() { - list($wizard, $configuration, $ldap, $access) - = $this->getWizardAndMocks(); - - $configuration->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapEmailAttribute') { - return 'myEmailAttribute'; - } else { - //for requirement checks - return 'let me pass'; - } - }); - - $access->expects($this->exactly(3)) - ->method('combineFilterWithAnd') - ->willReturnCallback(function ($filterParts) { - return str_replace('=*', '', array_pop($filterParts)); - }); - - $access->expects($this->exactly(3)) - ->method('countUsers') - ->willReturnCallback(function ($filter) { - if ($filter === 'myEmailAttribute') { - return 0; - } elseif ($filter === 'mail') { - return 3; - } elseif ($filter === 'mailPrimaryAddress') { - return 17; - } - throw new \Exception('Untested filter: ' . $filter); - }); - - $result = $wizard->detectEmailAttribute()->getResultArray(); - $this->assertSame('mailPrimaryAddress', - $result['changes']['ldap_email_attr']); - } - - public function testDetectEmailAttributeFind() { - list($wizard, $configuration, $ldap, $access) - = $this->getWizardAndMocks(); - - $configuration->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapEmailAttribute') { - return ''; - } else { - //for requirement checks - return 'let me pass'; - } - }); - - $access->expects($this->exactly(2)) - ->method('combineFilterWithAnd') - ->willReturnCallback(function ($filterParts) { - return str_replace('=*', '', array_pop($filterParts)); - }); - - $access->expects($this->exactly(2)) - ->method('countUsers') - ->willReturnCallback(function ($filter) { - if ($filter === 'myEmailAttribute') { - return 0; - } elseif ($filter === 'mail') { - return 3; - } elseif ($filter === 'mailPrimaryAddress') { - return 17; - } - throw new \Exception('Untested filter: ' . $filter); - }); - - $result = $wizard->detectEmailAttribute()->getResultArray(); - $this->assertSame('mailPrimaryAddress', - $result['changes']['ldap_email_attr']); - } - - public function testDetectEmailAttributeFindNothing() { - list($wizard, $configuration, $ldap, $access) - = $this->getWizardAndMocks(); - - $configuration->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapEmailAttribute') { - return 'myEmailAttribute'; - } else { - //for requirement checks - return 'let me pass'; - } - }); - - $access->expects($this->exactly(3)) - ->method('combineFilterWithAnd') - ->willReturnCallback(function ($filterParts) { - return str_replace('=*', '', array_pop($filterParts)); - }); - - $access->expects($this->exactly(3)) - ->method('countUsers') - ->willReturnCallback(function ($filter) { - if ($filter === 'myEmailAttribute') { - return 0; - } elseif ($filter === 'mail') { - return 0; - } elseif ($filter === 'mailPrimaryAddress') { - return 0; - } - throw new \Exception('Untested filter: ' . $filter); - }); - - $result = $wizard->detectEmailAttribute(); - $this->assertSame(false, $result->hasChanges()); - } - - public function testCumulativeSearchOnAttributeSkipReadDN() { - // tests that there is no infinite loop, when skipping already processed - // DNs (they can be returned multiple times for multiple filters ) - list($wizard, $configuration, $ldap) = $this->getWizardAndMocks(); - - $configuration->expects($this->any()) - ->method('__get') - ->willReturnCallback(function ($name) { - if ($name === 'ldapBase') { - return ['base']; - } - return null; - }); - - $this->prepareLdapWrapperForConnections($ldap); - - $ldap->expects($this->any()) - ->method('isResource') - ->willReturnCallback(function ($res) { - return (bool)$res; - }); - - $ldap->expects($this->any()) - ->method('search') - //dummy value, usually invalid - ->willReturn(true); - - $ldap->expects($this->any()) - ->method('countEntries') - //an is_resource check will follow, so we need to return a dummy resource - ->willReturn(7); - - //5 DNs per filter means 2x firstEntry and 8x nextEntry - $ldap->expects($this->any()) - ->method('firstEntry') - //dummy value, usually invalid - ->willReturn(1); - - global $mark; - $mark = false; - // entries return order: 1, 2, 3, 4, 4, 5, 6 - $ldap->expects($this->any()) - ->method('nextEntry') - //dummy value, usually invalid - ->willReturnCallback(function ($a, $prev) { - $current = $prev + 1; - if ($current === 7) { - return false; - } - global $mark; - if ($prev === 4 && !$mark) { - $mark = true; - return 4; - } - return $current; - }); - - $ldap->expects($this->any()) - ->method('getAttributes') - //dummy value, usually invalid - ->willReturnCallback(function ($a, $entry) { - return ['cn' => [$entry], 'count' => 1]; - }); - - $ldap->expects($this->any()) - ->method('getDN') - //dummy value, usually invalid - ->willReturnCallback(function ($a, $b) { - return $b; - }); - - // The following expectations are the real test - $filters = ['f1', 'f2', '*']; - $resultArray = $wizard->cumulativeSearchOnAttribute($filters, 'cn', 0); - $this->assertSame(6, count($resultArray)); - unset($mark); - } -}