diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d7f95bd..1f12061 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -7,7 +7,7 @@ on:
- master
- stable*
schedule:
- - cron: '0 2 * * *'
+ - cron: "0 2 * * *"
env:
APP_NAME: auto_groups
@@ -22,13 +22,12 @@ jobs:
strategy:
fail-fast: false
matrix:
- php-versions: ['8.2', '8.3']
- databases: ['sqlite']
- server-versions:
- ['stable30', 'stable31', 'stable32']
+ php-versions: ["8.2", "8.3"]
+ databases: ["sqlite"]
+ server-versions: ["stable31", "stable32", "stable33"]
experimental: [false]
include:
- - php-versions: '8.4'
+ - php-versions: "8.4"
databases: sqlite
server-versions: master
experimental: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c5e4fb..58f5c04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## 1.7.0 - 2026-03-20
+
+### Changed
+
+- Migrate event listener registration to `IBootstrap`/`registerEventListener()` for compatibility with NC34, replacing the deprecated `IEventDispatcher::addListener()` approach
+- Compatibility up to NC34
+
+### Fixed
+
+- Restore login hook test which was previously broken due to hook config being read at app instantiation time rather than at event dispatch time
+
## 1.6.2 - 2025-03-03
### Fixed
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..a68890d
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,62 @@
+# Auto Groups - Nextcloud App
+
+## Overview
+
+A Nextcloud app (v1.7.0, AGPL-3.0) that automatically adds users to configured groups ("Auto Groups"), with optional exemptions for users in "Override Groups". A modernized fork of the abandoned [defaultgroup](https://github.com/bodangren/defaultgroup) app.
+
+- **Nextcloud compatibility**: 31–34
+- **PHP**: 8.2, 8.3
+- **App ID**: `auto_groups` (note: older config used `AutoGroups` — migration logic exists)
+
+## Architecture
+
+Single-class app with minimal footprint:
+
+- `lib/AutoGroupsManager.php` — core logic; registers event listeners and handles group assignment/deletion
+- `lib/AppInfo/Application.php` — bootstraps the app via Nextcloud's DI container
+- `lib/Settings/Admin.php` — admin settings page
+- `appinfo/routes.php` — routes
+- `templates/admin.php` + `css/admin.css` + `js/admin.js` — admin UI
+
+## Key Behavior
+
+**Event hooks** (configurable):
+- `creation_hook` (default: on) — fires on `UserCreatedEvent` and `UserFirstTimeLoggedInEvent`
+- `modification_hook` (default: on) — fires on `UserAddedEvent` and `UserRemovedEvent`
+- `login_hook` (default: off) — fires on `PostLoginEvent` and `UserLoggedInEvent`; useful for external user backends
+
+**Group assignment logic** (`addAndRemoveAutoGroups`):
+- If user belongs to any Override Group → remove from all Auto Groups
+- If user belongs to no Override Group → add to all Auto Groups
+
+**Group deletion protection**: throws `OCSBadRequestException` if trying to delete a group referenced as an Auto Group or Override Group.
+
+## Config Keys
+
+Stored via Nextcloud's `IConfig` under app `auto_groups`:
+- `auto_groups` — JSON array of group IDs
+- `override_groups` — JSON array of group IDs
+- `creation_hook` — `'true'`/`'false'`
+- `modification_hook` — `'true'`/`'false'`
+- `login_hook` — `'true'`/`'false'`
+
+## Testing
+
+- Unit tests: `tests/Unit/` (uses PHPUnit mocks, extends Nextcloud's `Test\TestCase`)
+- Integration tests: `tests/Integration/`
+- Manual testing: `tests/Docker/run-docker-test-instance.sh` spins up a Docker instance on port 8080
+- Lint: `composer run lint` (runs `php -l` on all PHP files)
+
+## Release Process
+
+Releases are handled by `.github/workflows/release.yml` on GitHub release publication:
+1. Verifies `appinfo/info.xml` version matches the git tag
+2. Verifies `CHANGELOG.md` has an entry for the version
+3. Packages and uploads to GitHub Releases
+4. Submits to Nextcloud App Store (requires `AUTO_GROUPS_SIGNING_KEY` and `APP_STORE_API_TOKEN` secrets)
+
+## Noteworthy
+
+- **Config namespace migration**: The app previously used `AutoGroups` as the config namespace instead of `auto_groups`. Migration code in `AutoGroupsManager::__construct` handles upgrading old configs (see GitHub issue #82).
+- **l10n**: Translations managed via Transifex (`.tx/config`); many languages supported.
+- No Composer dependencies beyond dev tooling — the app relies entirely on Nextcloud's built-in OCP APIs.
diff --git a/README.md b/README.md
index d320ee2..17eca83 100644
--- a/README.md
+++ b/README.md
@@ -4,12 +4,12 @@ Automatically add users to specified Auto Groups, except for those belonging to
## Test Status
-| Nextcloud Server Branch | Unit & Integration Tests | Code Coverage |
+| Nextcloud Server Branch | Unit & Integration Tests | Code Coverage |
| ------------------------------------------------------------- | :-----------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------: |
-| [stable30](https://github.com/nextcloud/server/tree/stable30) |  | [](https://codecov.io/gh/stjosh/auto_groups) |
-| [stable31](https://github.com/nextcloud/server/tree/stable31) |  | [](https://codecov.io/gh/stjosh/auto_groups) |
-| [stable32](https://github.com/nextcloud/server/tree/stable32) |  | [](https://codecov.io/gh/stjosh/auto_groups) |
-| [master](https://github.com/nextcloud/server/tree/master) |  | [](https://codecov.io/gh/stjosh/auto_groups) |
+| [stable31](https://github.com/nextcloud/server/tree/stable31) |  | [](https://codecov.io/gh/stjosh/auto_groups) |
+| [stable32](https://github.com/nextcloud/server/tree/stable32) |  | [](https://codecov.io/gh/stjosh/auto_groups) |
+| [stable33](https://github.com/nextcloud/server/tree/stable33) |  | [](https://codecov.io/gh/stjosh/auto_groups) |
+| [master](https://github.com/nextcloud/server/tree/master) (NC34, experimental) |  | [](https://codecov.io/gh/stjosh/auto_groups) |
Unit and Integration Tests are executed with PHP v8.2 and v8.3.
@@ -35,8 +35,6 @@ and then access your test instance on http://localhost:8080. The `auto_groups` a
- [Everyone Group](https://apps.nextcloud.com/apps/group_everyone): The "Everyone Group" app adds a virtual Group Backend, always returning all users. In contrast, "Auto Groups" operates on "real" groups in your normal Group Backend. Additionally, it is possible to specify Override Groups which will prevent users from being added to the Auto Group(s).
- [Default Group](https://apps.nextcloud.com/apps/defaultgroup): "Auto Groups" is actually a modernized and maintaned fork of "Default Group", which seems to be abandoned since NC12 or so. In terms of functionality, they are almost identical.
-In addition, I plan to add some more features over time, e.g., "Union Groups" - see the [Milestone Plans](https://github.com/stjosh/auto_groups/milestones) for more details.
-
## Issue Tracker / Contributions
Contributions are welcome on [GitHub](https://github.com/stjosh/auto_groups/issues).
diff --git a/appinfo/info.xml b/appinfo/info.xml
index f55a5e6..d2f7aac 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -22,11 +22,10 @@ Note that this app prevents group deletions for groups referenced as Auto Groups
* [Everyone Group](https://apps.nextcloud.com/apps/group_everyone): The "Everyone Group" app adds a virtual Group Backend, always returning all users. In contrast, "Auto Groups" operates on "real" groups in your normal Group Backend. Additionally, it is possible to specify Override Groups which will prevent users from being added to the Auto Group(s).
* [Default Group](https://apps.nextcloud.com/apps/defaultgroup): "Auto Groups" is actually a modernized and maintaned fork of "Default Group", which seems to be abandoned since NC12 or so. In terms of functionality, they are almost identical.
-In addition, I plan to add some more features over time, e.g., "Union Groups" - see the [Milestone Plans](https://github.com/stjosh/auto_groups/milestones) for more details.
- 1.6.2
+ 1.7.0
agpl
- Josua Hunziker
+ Josua Hunziker
AutoGroups
tools
https://github.com/stjosh/auto_groups
@@ -34,7 +33,7 @@ In addition, I plan to add some more features over time, e.g., "Union Groups" -
https://github.com/stjosh/auto_groups.git
https://raw.githubusercontent.com/stjosh/auto_groups/master/screenshots/settings.png
-
+
OCA\AutoGroups\Settings\Admin
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 19be8d1..458b1f3 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -1,11 +1,24 @@
OCA\AutoGroupsGroups\Controller\PageController->index()
+ * @copyright Copyright (c) 2020
+ *
+ * @author Josua Hunziker
+ *
+ * @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
*
- * The controller class has to be registered in the application.php file since
- * it's instantiated in there
*/
return [
'routes' => [
diff --git a/css/admin.css b/css/admin.css
index 485f063..1fe0106 100644
--- a/css/admin.css
+++ b/css/admin.css
@@ -1,3 +1,24 @@
+/**
+ * @copyright Copyright (c) 2020
+ *
+ * @author Josua Hunziker
+ *
+ * @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
+ *
+ */
+
#auto_groups_options {
position: relative;
}
diff --git a/js/admin.js b/js/admin.js
index 5ecb8d8..df36ee6 100644
--- a/js/admin.js
+++ b/js/admin.js
@@ -1,14 +1,23 @@
/**
- * Copyright (c) 2020
+ * @copyright Copyright (c) 2020
*
- * @author Josua Hunziker
+ * @author Josua Hunziker
*
* Based on the work of Ján Stibila and Lukas Reschke
*
- * This file is licensed under the Affero General Public License version 3
- * or later.
+ * @license AGPL-3.0
*
- * See the COPYING-README file.
+ * 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
*
*/
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 438af37..a48d1a3 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -2,8 +2,8 @@
/**
* @copyright Copyright (c) 2020
*
- * @author Josua Hunziker
- *
+ * @author Josua Hunziker
+ *
* Based on the work of Ján Stibila
*
* @license AGPL-3.0
@@ -25,17 +25,41 @@
namespace OCA\AutoGroups\AppInfo;
use OCP\AppFramework\App;
+use OCP\AppFramework\Bootstrap\IBootContext;
+use OCP\AppFramework\Bootstrap\IBootstrap;
+use OCP\AppFramework\Bootstrap\IRegistrationContext;
+use OCP\User\Events\UserCreatedEvent;
+use OCP\User\Events\UserFirstTimeLoggedInEvent;
+use OCP\User\Events\PostLoginEvent;
+use OCP\User\Events\UserLoggedInEvent;
+use OCP\Group\Events\UserAddedEvent;
+use OCP\Group\Events\UserRemovedEvent;
+use OCP\Group\Events\BeforeGroupDeletedEvent;
+
use OCA\AutoGroups\AutoGroupsManager;
+use OCA\AutoGroups\Listener\AutoGroupsListener;
-class Application extends App {
+class Application extends App implements IBootstrap
+{
+ public function __construct()
+ {
+ parent::__construct('auto_groups');
+ }
- private $autoGroupsManager;
+ public function register(IRegistrationContext $context): void
+ {
+ $context->registerEventListener(UserCreatedEvent::class, AutoGroupsListener::class);
+ $context->registerEventListener(UserFirstTimeLoggedInEvent::class, AutoGroupsListener::class);
+ $context->registerEventListener(UserAddedEvent::class, AutoGroupsListener::class);
+ $context->registerEventListener(UserRemovedEvent::class, AutoGroupsListener::class);
+ $context->registerEventListener(PostLoginEvent::class, AutoGroupsListener::class);
+ $context->registerEventListener(UserLoggedInEvent::class, AutoGroupsListener::class);
+ $context->registerEventListener(BeforeGroupDeletedEvent::class, AutoGroupsListener::class);
+ }
- /**
- * Application constructor.
- */
- public function __construct() {
- parent::__construct('auto_groups');
- $this->autoGroupsManager = $this->getContainer()->query(AutoGroupsManager::class);
+ public function boot(IBootContext $context): void
+ {
+ // Instantiate AutoGroupsManager to trigger legacy config migration
+ $context->getAppContainer()->query(AutoGroupsManager::class);
}
}
diff --git a/lib/AutoGroupsManager.php b/lib/AutoGroupsManager.php
index b9d4f2a..9bbd2f0 100644
--- a/lib/AutoGroupsManager.php
+++ b/lib/AutoGroupsManager.php
@@ -3,8 +3,8 @@
/**
* @copyright Copyright (c) 2020
*
- * @author Josua Hunziker
- *
+ * @author Josua Hunziker
+ *
* Based on the work of Ján Stibila
*
* @license AGPL-3.0
@@ -25,16 +25,7 @@
namespace OCA\AutoGroups;
-use OCP\User\Events\UserCreatedEvent;
-use OCP\User\Events\UserFirstTimeLoggedInEvent;
-use OCP\User\Events\PostLoginEvent;
-use OCP\User\Events\UserLoggedInEvent;
-use OCP\Group\Events\UserAddedEvent;
-use OCP\Group\Events\UserRemovedEvent;
-use OCP\Group\Events\BeforeGroupDeletedEvent;
-
use OCP\IGroupManager;
-use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IL10N;
@@ -50,9 +41,9 @@ class AutoGroupsManager
private $l;
/**
- * Listener manager constructor.
+ * AutoGroupsManager constructor.
*/
- public function __construct(IGroupManager $groupManager, IEventDispatcher $eventDispatcher, IConfig $config, LoggerInterface $logger, IL10N $l)
+ public function __construct(IGroupManager $groupManager, IConfig $config, LoggerInterface $logger, IL10N $l)
{
$this->groupManager = $groupManager;
$this->logger = $logger;
@@ -96,39 +87,10 @@ public function __construct(IGroupManager $groupManager, IEventDispatcher $event
$this->config->deleteAppValue("AutoGroups", "override_groups");
}
}
-
- // The callback as a PHP callable
- $groupAssignmentCallback = [$this, 'addAndRemoveAutoGroups'];
-
- // Get the hook configs
- $creationHook = $this->config->getAppValue("auto_groups", "creation_hook", 'true');
- $modificationHook = $this->config->getAppValue("auto_groups", "modification_hook", 'true');
- $loginHook = $this->config->getAppValue("auto_groups", "login_hook", 'false');
-
- // If creation hook is enabled, add user to / remove user from auto groups on creation
- if (filter_var($creationHook, FILTER_VALIDATE_BOOLEAN)) {
- $eventDispatcher->addListener(UserCreatedEvent::class, $groupAssignmentCallback);
- $eventDispatcher->addListener(UserFirstTimeLoggedInEvent::class, $groupAssignmentCallback);
- }
-
- // If modification hook is enabled, add user to / remove user from auto groups on every modification of user groups
- if (filter_var($modificationHook, FILTER_VALIDATE_BOOLEAN)) {
- $eventDispatcher->addListener(UserAddedEvent::class, $groupAssignmentCallback);
- $eventDispatcher->addListener(UserRemovedEvent::class, $groupAssignmentCallback);
- }
-
- // If login hook is enabled, add user to / remove user from auto groups on every successful login
- if (filter_var($loginHook, FILTER_VALIDATE_BOOLEAN)) {
- $eventDispatcher->addListener(PostLoginEvent::class, $groupAssignmentCallback);
- $eventDispatcher->addListener(UserLoggedInEvent::class, $groupAssignmentCallback);
- }
-
- // Handle group deletion events
- $eventDispatcher->addListener(BeforeGroupDeletedEvent::class, [$this, 'handleGroupDeletion']);
}
/**
- * The event handler to check group assignmnet for a user
+ * The event handler to check group assignment for a user
*/
public function addAndRemoveAutoGroups($event)
{
@@ -166,9 +128,9 @@ public function addAndRemoveAutoGroups($event)
/**
* The event handler to handle group deletions
- *
+ *
* @throws OCSBadRequestException
- *
+ *
*/
public function handleGroupDeletion($event)
{
@@ -178,7 +140,7 @@ public function handleGroupDeletion($event)
$allGroupNames = array_merge($groupNames, $overrideGroupNames);
- // Get group name of group do delete
+ // Get group name of group to delete
$groupNameToDelete = $event->getGroup()->getGID();
// Prevent deletion if group to delete is configured in AutoGroups
diff --git a/lib/Listener/AutoGroupsListener.php b/lib/Listener/AutoGroupsListener.php
new file mode 100644
index 0000000..23d7f93
--- /dev/null
+++ b/lib/Listener/AutoGroupsListener.php
@@ -0,0 +1,65 @@
+
+ *
+ * @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\AutoGroups\Listener;
+
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\User\Events\UserCreatedEvent;
+use OCP\User\Events\UserFirstTimeLoggedInEvent;
+use OCP\User\Events\PostLoginEvent;
+use OCP\User\Events\UserLoggedInEvent;
+use OCP\Group\Events\UserAddedEvent;
+use OCP\Group\Events\UserRemovedEvent;
+use OCP\Group\Events\BeforeGroupDeletedEvent;
+use OCP\IConfig;
+
+use OCA\AutoGroups\AutoGroupsManager;
+
+/** @template-implements IEventListener */
+class AutoGroupsListener implements IEventListener
+{
+ public function __construct(
+ private AutoGroupsManager $manager,
+ private IConfig $config
+ ) {}
+
+ public function handle(Event $event): void
+ {
+ if ($event instanceof UserCreatedEvent || $event instanceof UserFirstTimeLoggedInEvent) {
+ if (filter_var($this->config->getAppValue('auto_groups', 'creation_hook', 'true'), FILTER_VALIDATE_BOOLEAN)) {
+ $this->manager->addAndRemoveAutoGroups($event);
+ }
+ } elseif ($event instanceof UserAddedEvent || $event instanceof UserRemovedEvent) {
+ if (filter_var($this->config->getAppValue('auto_groups', 'modification_hook', 'true'), FILTER_VALIDATE_BOOLEAN)) {
+ $this->manager->addAndRemoveAutoGroups($event);
+ }
+ } elseif ($event instanceof PostLoginEvent || $event instanceof UserLoggedInEvent) {
+ if (filter_var($this->config->getAppValue('auto_groups', 'login_hook', 'false'), FILTER_VALIDATE_BOOLEAN)) {
+ $this->manager->addAndRemoveAutoGroups($event);
+ }
+ } elseif ($event instanceof BeforeGroupDeletedEvent) {
+ $this->manager->handleGroupDeletion($event);
+ }
+ }
+}
diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php
index 37e485d..74eb839 100644
--- a/lib/Settings/Admin.php
+++ b/lib/Settings/Admin.php
@@ -2,7 +2,7 @@
/**
* @copyright Copyright (c) 2020
*
- * @author Josua Hunziker
+ * @author Josua Hunziker
*
* Based on the work of Ján Stibila
*
diff --git a/templates/admin.php b/templates/admin.php
index 382f98c..495b217 100644
--- a/templates/admin.php
+++ b/templates/admin.php
@@ -3,7 +3,7 @@
/**
* @copyright Copyright (c) 2020
*
- * @author Josua Hunziker
+ * @author Josua Hunziker
*
* Based on the work of Ján Stibila
*
diff --git a/tests/Integration/AdminSettingsTest.php b/tests/Integration/AdminSettingsTest.php
index a095ef1..aadf963 100644
--- a/tests/Integration/AdminSettingsTest.php
+++ b/tests/Integration/AdminSettingsTest.php
@@ -3,7 +3,7 @@
/**
* @copyright Copyright (c) 2020
*
- * @author Josua Hunziker
+ * @author Josua Hunziker
*
* @license GNU AGPL version 3 or any later version
*
@@ -49,13 +49,14 @@ protected function setUp(): void
$this->app = new Application();
$this->container = $this->app->getContainer();
- $this->settingsManager = $this->container->getServer()->getSettingsManager();
+ $this->settingsManager = $this->container->query(IManager::class);
}
public function testAppSettingsExist()
{
$settings = $this->settingsManager->getAdminSettings('additional');
+ // The app must register its Admin settings class at priority 100 in the 'additional' section
$this->assertArrayHasKey(100, $settings);
$this->assertIsArray($settings[100]);
$adminSettings = $settings[100][0];
@@ -66,6 +67,7 @@ public function testFormRender()
{
$appSettings = $this->settingsManager->getAdminSettings('additional')[100][0];
+ // getForm() must return a TemplateResponse (the actual template rendering is not tested here)
$templateResponse = $appSettings->getForm();
$this->assertInstanceOf(TemplateResponse::class, $templateResponse);
diff --git a/tests/Integration/EventsTest.php b/tests/Integration/EventsTest.php
index 71b94c8..f18182c 100644
--- a/tests/Integration/EventsTest.php
+++ b/tests/Integration/EventsTest.php
@@ -3,7 +3,7 @@
/**
* @copyright Copyright (c) 2020
*
- * @author Josua Hunziker
+ * @author Josua Hunziker
*
* @license GNU AGPL version 3 or any later version
*
@@ -28,7 +28,8 @@
use OCP\IUserManager;
use OCP\IGroupManager;
use OCP\IConfig;
-use OCP\IUserSession;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\User\Events\PostLoginEvent;
use OCP\AppFramework\OCS\OCSBadRequestException;
@@ -46,7 +47,7 @@ class EventsTest extends TestCase
private $userManager;
private $groupManager;
private $config;
- private $userSession;
+ private $eventDispatcher;
private $backend;
@@ -60,7 +61,7 @@ protected function setUp(): void
$this->groupManager = $this->container->query(IGroupManager::class);
$this->userManager = $this->container->query(IUserManager::class);
$this->config = $this->container->query(IConfig::class);
- $this->userSession = $this->container->query(IUserSession::class);
+ $this->eventDispatcher = $this->container->query(IEventDispatcher::class);
$this->backend = $this->groupManager->getBackends()[0];
@@ -82,6 +83,7 @@ public function testCreateHook()
$this->config->setAppValue("auto_groups", "creation_hook", 'true');
$this->config->setAppValue("auto_groups", "modification_hook", 'true');
+ // Creating a user should immediately add them to auto groups (creation_hook=true)
$this->userManager->createUser('testuser', 'testPassword');
$testUser = $this->userManager->get('testuser');
@@ -101,6 +103,7 @@ public function testAddHook()
$overridegroup = $this->groupManager->search('overridegroup1')[0];
$autogroup = $this->groupManager->search('autogroup1')[0];
+ // Adding user to an override group should remove them from auto groups (modification_hook=true)
$overridegroup->addUser($testUser);
$this->assertNotTrue($autogroup->inGroup($testUser));
@@ -119,31 +122,43 @@ public function testRemoveHook()
$autogroup1 = $this->groupManager->search('autogroup1')[0];
$autogroup2 = $this->groupManager->search('autogroup2')[0];
+ // Removing user from the override group should re-add them to all auto groups (modification_hook=true)
$overridegroup->removeUser($testUser);
$this->assertTrue($autogroup1->inGroup($testUser) && $autogroup2->inGroup($testUser));
}
- /*public function testLoginHook()
+ public function testLoginHook()
{
$this->config->setAppValue("auto_groups", "auto_groups", '["autogroup1", "autogroup2"]');
$this->config->setAppValue("auto_groups", "override_groups", '["overridegroup1"]');
$this->config->setAppValue("auto_groups", "login_hook", 'true');
$this->config->setAppValue("auto_groups", "creation_hook", 'false');
$this->config->setAppValue("auto_groups", "modification_hook", 'false');
-
- $testUser = $this->userManager->get('testuser');
- $overridegroup = $this->groupManager->search('overridegroup1')[0];
+
+ // Use a dedicated user for this test to avoid state from other tests.
+ // IUserSession::login() cannot be used in CLI test context (no HTTP session),
+ // so we dispatch PostLoginEvent via the Nextcloud event dispatcher directly —
+ // the same mechanism Nextcloud uses internally for all other events in this test suite.
+ $loginUser = $this->userManager->createUser('loginuser', 'testPassword');
$autogroup1 = $this->groupManager->search('autogroup1')[0];
$autogroup2 = $this->groupManager->search('autogroup2')[0];
+ $overridegroup = $this->groupManager->search('overridegroup1')[0];
- $this->assertTrue($autogroup1->inGroup($testUser) && $autogroup2->inGroup($testUser));
-
- $overridegroup->addUser($testUser);
- $this->userSession->login('testuser', 'testPassword');
-
- $this->assertTrue(!$autogroup1->inGroup($testUser) && !$autogroup2->inGroup($testUser));
- }*/
+ // Phase 1: user is not in override group → login should ADD them to auto groups
+ $this->assertFalse($autogroup1->inGroup($loginUser));
+ $this->eventDispatcher->dispatchTyped(new PostLoginEvent($loginUser, 'loginuser', 'testPassword', false));
+ $this->assertTrue($autogroup1->inGroup($loginUser) && $autogroup2->inGroup($loginUser));
+
+ // Phase 2: add user to override group (modification_hook=false so no auto-trigger),
+ // then login should REMOVE them from auto groups.
+ // Re-fetch group objects to avoid stale per-Group $users cache from Phase 1.
+ $overridegroup->addUser($loginUser);
+ $this->eventDispatcher->dispatchTyped(new PostLoginEvent($loginUser, 'loginuser', 'testPassword', false));
+ $freshGroup1 = $this->groupManager->get('autogroup1');
+ $freshGroup2 = $this->groupManager->get('autogroup2');
+ $this->assertFalse($freshGroup1->inGroup($loginUser) || $freshGroup2->inGroup($loginUser));
+ }
public function testBeforeGroupDeletionHook()
@@ -156,6 +171,7 @@ public function testBeforeGroupDeletionHook()
$autogroup = $this->groupManager->search('autogroup1')[0];
+ // Deleting a group that is configured as an auto group should be prevented
$this->expectException(OCSBadRequestException::class);
$autogroup->delete();
}
diff --git a/tests/Unit/AdminSettingsTest.php b/tests/Unit/AdminSettingsTest.php
index de39a28..62a40f8 100644
--- a/tests/Unit/AdminSettingsTest.php
+++ b/tests/Unit/AdminSettingsTest.php
@@ -3,7 +3,7 @@
/**
* @copyright Copyright (c) 2020
*
- * @author Josua Hunziker
+ * @author Josua Hunziker
*
* @license GNU AGPL version 3 or any later version
*
@@ -71,16 +71,19 @@ protected function setUp(): void
public function testSection()
{
+ // Settings must appear in the 'additional' admin section
$this->assertEquals('additional', $this->adminSettings->getSection());
}
public function testPriority()
{
+ // Priority 100 places the form towards the bottom of the section
$this->assertEquals(100, $this->adminSettings->getPriority());
}
public function testForm()
{
+ // getForm() must read all five config values and pass them to the template as parameters
$this->config->expects($this->exactly(5))
->method('getAppValue')
->withConsecutive(
diff --git a/tests/Unit/AutoGroupsManagerTest.php b/tests/Unit/AutoGroupsManagerTest.php
index a10ee47..4c38da4 100644
--- a/tests/Unit/AutoGroupsManagerTest.php
+++ b/tests/Unit/AutoGroupsManagerTest.php
@@ -3,7 +3,7 @@
/**
* @copyright Copyright (c) 2020
*
- * @author Josua Hunziker
+ * @author Josua Hunziker
*
* @license GNU AGPL version 3 or any later version
*
@@ -24,16 +24,10 @@
namespace OCA\AutoGroups\Tests\Unit;
-use OCP\User\Events\UserCreatedEvent;
-use OCP\User\Events\UserFirstTimeLoggedInEvent;
-use OCP\User\Events\UserLoggedInEvent;
-use OCP\User\Events\PostLoginEvent;
-use OCP\Group\Events\UserAddedEvent;
-use OCP\Group\Events\UserRemovedEvent;
use OCP\Group\Events\BeforeGroupDeletedEvent;
+use OCP\User\Events\UserCreatedEvent;
use OCP\IGroupManager;
-use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IL10N;
@@ -52,7 +46,6 @@
class AutoGroupsManagerTest extends TestCase
{
private $groupManager;
- private $eventDispatcher;
private $config;
private $logger;
private $il10n;
@@ -62,7 +55,6 @@ protected function setUp(): void
parent::setUp();
$this->groupManager = $this->createMock(IGroupManager::class);
- $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->config = $this->createMock(IConfig::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->il10n = $this->createMock(IL10N::class);
@@ -73,58 +65,34 @@ protected function setUp(): void
->willReturn('Test User');
}
- private function createAutoGroupsManager(
- $auto_groups = [],
- $override_groups = [],
- $creation_hook = 'true',
- $modification_hook = 'true',
- $login_hook = 'false',
- $expectedNumberOfConfigCalls = 7
- ) {
- $this->config->expects($this->exactly($expectedNumberOfConfigCalls))
- ->method('getAppValue')
- ->withConsecutive(
- ['AutoGroups', 'creation_only'],
- ['AutoGroups', 'creation_hook'],
- ['auto_groups', 'creation_hook', 'true'],
- ['auto_groups', 'modification_hook', 'true'],
- ['auto_groups', 'login_hook', 'false'],
- ['auto_groups', 'auto_groups', '[]'],
- ['auto_groups', 'override_groups', '[]']
- )
- ->willReturnOnConsecutiveCalls('', '', $creation_hook, $modification_hook, $login_hook, json_encode($auto_groups), json_encode($override_groups));
-
- return new AutoGroupsManager($this->groupManager, $this->eventDispatcher, $this->config, $this->logger, $this->il10n);
- }
-
- private function initEventHandlerTests($auto_groups = [], $override_groups = [])
+ private function createAutoGroupsManager($auto_groups = [], $override_groups = [])
{
- $this->eventDispatcher->expects($this->exactly(5))
- ->method('addListener')
- ->withConsecutive(
- [UserCreatedEvent::class, $this->callback('is_callable')],
- [UserFirstTimeLoggedInEvent::class, $this->callback('is_callable')],
- [UserAddedEvent::class, $this->callback('is_callable')],
- [UserRemovedEvent::class, $this->callback('is_callable')],
- [BeforeGroupDeletedEvent::class, $this->callback('is_callable')]
- );
-
- $agm = $this->createAutoGroupsManager($auto_groups, $override_groups);
- return $agm;
+ $this->config->method('getAppValue')
+ ->willReturnCallback(function ($app, $key, $default = '') use ($auto_groups, $override_groups) {
+ if ($app === 'AutoGroups') {
+ return ''; // no migration needed
+ }
+ if ($app === 'auto_groups' && $key === 'auto_groups') {
+ return json_encode($auto_groups);
+ }
+ if ($app === 'auto_groups' && $key === 'override_groups') {
+ return json_encode($override_groups);
+ }
+ return $default;
+ });
+
+ return new AutoGroupsManager($this->groupManager, $this->config, $this->logger, $this->il10n);
}
private function configMigrationTestImpl($creationOnly, $expectedModification)
{
- $this->config->expects($this->exactly(5))
+ $this->config->expects($this->exactly(2))
->method('getAppValue')
->withConsecutive(
['AutoGroups', 'creation_only'],
['AutoGroups', 'creation_hook'],
- ['auto_groups', 'creation_hook', 'true'],
- ['auto_groups', 'modification_hook', 'true'],
- ['auto_groups', 'login_hook', 'false'],
)
- ->willReturnOnConsecutiveCalls($creationOnly, '', 'true', 'true', 'false');
+ ->willReturnOnConsecutiveCalls($creationOnly, '');
$this->config->expects($this->exactly(1))
->method('setAppValue')
@@ -134,65 +102,7 @@ private function configMigrationTestImpl($creationOnly, $expectedModification)
->method('deleteAppValue')
->with('AutoGroups', 'creation_only');
- return new AutoGroupsManager($this->groupManager, $this->eventDispatcher, $this->config, $this->logger, $this->il10n);
- }
-
- public function testCreatedAddedRemovedHooksWithDefaultSettings()
- {
- $this->eventDispatcher->expects($this->exactly(5))
- ->method('addListener')
- ->withConsecutive(
- [UserCreatedEvent::class, $this->callback('is_callable')],
- [UserFirstTimeLoggedInEvent::class, $this->callback('is_callable')],
- [UserAddedEvent::class, $this->callback('is_callable')],
- [UserRemovedEvent::class, $this->callback('is_callable')],
- [BeforeGroupDeletedEvent::class, $this->callback('is_callable')]
- );
-
- $agm = $this->createAutoGroupsManager([], [], 'true', 'true', 'false', 5);
- }
-
- public function testAlsoLoginHookIfEnabled()
- {
- $this->eventDispatcher->expects($this->exactly(7))
- ->method('addListener')
- ->withConsecutive(
- [UserCreatedEvent::class, $this->callback('is_callable')],
- [UserFirstTimeLoggedInEvent::class, $this->callback('is_callable')],
- [UserAddedEvent::class, $this->callback('is_callable')],
- [UserRemovedEvent::class, $this->callback('is_callable')],
- [PostLoginEvent::class, $this->callback('is_callable')],
- [UserLoggedInEvent::class, $this->callback('is_callable')],
- [BeforeGroupDeletedEvent::class, $this->callback('is_callable')]
- );
-
- $agm = $this->createAutoGroupsManager([], [], 'true', 'true', 'true', 5);
- }
-
- public function testCreationOnlyMode()
- {
- $this->eventDispatcher->expects($this->exactly(3))
- ->method('addListener')
- ->withConsecutive(
- [UserCreatedEvent::class, $this->callback('is_callable')],
- [UserFirstTimeLoggedInEvent::class, $this->callback('is_callable')],
- [BeforeGroupDeletedEvent::class, $this->callback('is_callable')]
- );
-
- $agm = $this->createAutoGroupsManager([], [], 'true', 'false', 'false', 5);
- }
-
- public function testModificationOnlyMode()
- {
- $this->eventDispatcher->expects($this->exactly(3))
- ->method('addListener')
- ->withConsecutive(
- [UserAddedEvent::class, $this->callback('is_callable')],
- [UserRemovedEvent::class, $this->callback('is_callable')],
- [BeforeGroupDeletedEvent::class, $this->callback('is_callable')]
- );
-
- $agm = $this->createAutoGroupsManager([], [], 'false', 'true', 'false', 5);
+ return new AutoGroupsManager($this->groupManager, $this->config, $this->logger, $this->il10n);
}
public function testAddingToAutoGroups()
@@ -202,6 +112,7 @@ public function testAddingToAutoGroups()
->method('getUser')
->willReturn($this->testUser);
+ // User belongs to no groups, so they should be added to the auto group
$this->groupManager->expects($this->once())
->method('getUserGroups')
->with($this->testUser)
@@ -217,7 +128,7 @@ public function testAddingToAutoGroups()
->with('autogroup')
->willReturn([$autogroup]);
- $agm = $this->initEventHandlerTests(['autogroup']);
+ $agm = $this->createAutoGroupsManager(['autogroup']);
$agm->addAndRemoveAutoGroups($event);
}
@@ -228,6 +139,7 @@ public function testAddingNotRequired()
->method('getUser')
->willReturn($this->testUser);
+ // User is already in the auto group, so addUser should never be called
$this->groupManager->expects($this->once())
->method('getUserGroups')
->with($this->testUser)
@@ -243,7 +155,7 @@ public function testAddingNotRequired()
->with('autogroup')
->willReturn([$autogroup]);
- $agm = $this->initEventHandlerTests(['autogroup']);
+ $agm = $this->createAutoGroupsManager(['autogroup']);
$agm->addAndRemoveAutoGroups($event);
}
@@ -254,6 +166,7 @@ public function testRemoveUserFromAutoGroups()
->method('getUser')
->willReturn($this->testUser);
+ // User belongs to an override group, so they should be removed from all auto groups
$this->groupManager->expects($this->once())
->method('getUserGroups')
->with($this->testUser)
@@ -269,7 +182,7 @@ public function testRemoveUserFromAutoGroups()
->withConsecutive(['autogroup1'], ['autogroup2'])
->willReturnOnConsecutiveCalls([$groupMock], [$groupMock]);
- $agm = $this->initEventHandlerTests(['autogroup1', 'autogroup2'], ['overridegroup1', 'overridegroup2']);
+ $agm = $this->createAutoGroupsManager(['autogroup1', 'autogroup2'], ['overridegroup1', 'overridegroup2']);
$agm->addAndRemoveAutoGroups($event);
}
@@ -280,6 +193,7 @@ public function testRemoveNotRequired()
->method('getUser')
->willReturn($this->testUser);
+ // User is in an override group but not in any auto group, so removeUser should never be called
$this->groupManager->expects($this->once())
->method('getUserGroups')
->with($this->testUser)
@@ -295,7 +209,7 @@ public function testRemoveNotRequired()
->withConsecutive(['autogroup1'], ['autogroup2'])
->willReturnOnConsecutiveCalls([$groupMock], [$groupMock]);
- $agm = $this->initEventHandlerTests(['autogroup1', 'autogroup2'], ['overridegroup1', 'overridegroup2']);
+ $agm = $this->createAutoGroupsManager(['autogroup1', 'autogroup2'], ['overridegroup1', 'overridegroup2']);
$agm->addAndRemoveAutoGroups($event);
}
@@ -311,9 +225,10 @@ public function testGroupDeletionPrevented()
->method('getGroup')
->willReturn($groupMock);
+ // autogroup2 is configured as an auto group, so deletion must be prevented
$this->expectException(OCSBadRequestException::class);
- $agm = $this->initEventHandlerTests(['autogroup1', 'autogroup2'], ['overridegroup1', 'overridegroup2']);
+ $agm = $this->createAutoGroupsManager(['autogroup1', 'autogroup2'], ['overridegroup1', 'overridegroup2']);
$agm->handleGroupDeletion($event);
}
@@ -329,17 +244,20 @@ public function testGroupDeletionPreventionNotNeeded()
->method('getGroup')
->willReturn($groupMock);
- $agm = $this->initEventHandlerTests(['autogroup1', 'autogroup2'], ['overridegroup1', 'overridegroup2']);
+ // 'some other group' is not referenced in config, so deletion should be allowed
+ $agm = $this->createAutoGroupsManager(['autogroup1', 'autogroup2'], ['overridegroup1', 'overridegroup2']);
$agm->handleGroupDeletion($event);
}
public function testConfigMigrationForCreationOnlyTrue()
{
+ // Legacy creation_only=true means modification_hook should be migrated to false
$agm = $this->configMigrationTestImpl('true', 'false');
}
public function testConfigMigrationForCreationOnlyFalse()
{
+ // Legacy creation_only=false means modification_hook should be migrated to true
$agm = $this->configMigrationTestImpl('false', 'true');
}
}
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index b332821..31e10d4 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -7,16 +7,12 @@
.
-
-
- ../
-
- ../tests
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+ ../lib
+
+
+
+
+
+