diff --git a/appinfo/info.xml b/appinfo/info.xml index 96e62910f..e4df38c90 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -176,6 +176,15 @@ Vrij en open source onder de EUPL-1.2-licentie. + + + OCA\LaunchPad\Activity\Settings\DashboardSharedSetting + OCA\LaunchPad\Activity\Settings\DashboardPublishedSetting + OCA\LaunchPad\Activity\Settings\DashboardUpdatedSetting + OCA\LaunchPad\Activity\Settings\DashboardAcknowledgedSetting + +
-

{{ t('launchpad', 'Storage backend') }}

-

- {{ - t( - 'launchpad', - 'Choose how LaunchPad stores dashboard content.', - ) - }} -

- - - - -
- - -

{{ t('launchpad', 'Group priority order') }}

{{ @@ -120,17 +49,17 @@

- -
+ +

{{ t('launchpad', 'Demo data') }}

- + -
+

{{ t('launchpad', 'Admin roles') }}

{{ @@ -150,12 +79,12 @@

- + -
+

{{ t('launchpad', 'Footer configuration') }}

{{ @@ -175,8 +104,8 @@

- -
+ +

{{ t('launchpad', 'All set') }}

{{ @@ -231,10 +160,11 @@ import { logger } from '../utils/logger.js' * SetupWizardModal — multi-step first-run wizard (REQ-WIZ-002). * * The shell owns navigation state; per-step bodies inline simple UI for - * Welcome / Storage / Done and embed canonical sibling components for - * Group order. Steps 4-6 render lightweight stubs because their sibling - * capabilities (demo-data-showcases, admin-roles, footer-customization) - * ship on parallel branches; the merge agent reconciles by replacing + * Welcome / Done and embed canonical sibling components for Group order. + * The storage step was retired (decision 131). Steps 3-5 render + * lightweight stubs because their sibling capabilities + * (demo-data-showcases, admin-roles, footer-customization) ship on + * parallel branches; the merge agent reconciles by replacing * the stub blocks with the real embeds. */ export default { @@ -252,10 +182,8 @@ export default { data() { return { currentStep: 1, - totalSteps: 7, + totalSteps: 6, loading: false, - storage: 'database', - groupfolderAvailable: false, completed: false, } }, @@ -276,8 +204,6 @@ export default { try { const { data } = await api.getSetupWizardState() if (data) { - this.storage = data.contentStorage || 'database' - this.groupfolderAvailable = !!data.groupfolderAvailable if (data.currentRecommendedStep && !data.complete) { this.currentStep = Math.max( 1, @@ -287,7 +213,7 @@ export default { } } catch (error) { // REQ-WIZ-008: a failed state read MUST NOT block the wizard. - // Default to Step 1 with a Database default. + // Default to Step 1. logger.warn('Failed to load wizard state', error) } }, @@ -299,10 +225,6 @@ export default { } this.loading = true try { - if (this.currentStep === 2) { - await api.setSetupWizardStorage(this.storage) - } - if (this.isFinalStep) { await api.completeSetupWizard() this.completed = true @@ -379,26 +301,6 @@ export default { margin: 0 0 8px; } -.setup-wizard__radio { - display: flex; - gap: 12px; - padding: 12px; - border: 1px solid var(--color-border); - border-radius: var(--border-radius); - cursor: pointer; -} - -.setup-wizard__radio--disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.setup-wizard__hint { - color: var(--color-text-maxcontrast); - margin: 4px 0 0; - font-size: 13px; -} - .setup-wizard__note { padding: 12px; background: var(--color-background-dark); diff --git a/src/modals/__tests__/SetupWizardModal.spec.js b/src/modals/__tests__/SetupWizardModal.spec.js index 1133ad915..b7871f858 100644 --- a/src/modals/__tests__/SetupWizardModal.spec.js +++ b/src/modals/__tests__/SetupWizardModal.spec.js @@ -3,9 +3,9 @@ * SPDX-License-Identifier: EUPL-1.2 * * Vitest unit tests for `SetupWizardModal.vue`. Covers REQ-WIZ-002 - * (multi-step shell), REQ-WIZ-003 (storage backend persistence on - * Step 2 → Next), REQ-WIZ-008 (state load on mount), and REQ-WIZ-009 - * (Finish triggers `completeSetupWizard`). + * (six-step shell; the storage step was retired by decision 131), + * REQ-WIZ-008 (state load on mount), and REQ-WIZ-009 (Finish triggers + * `completeSetupWizard`). * * The `api` module is mocked at the import boundary so no HTTP traffic * is generated. The embedded `GroupPriorityOrder` is stubbed to avoid @@ -20,7 +20,6 @@ import { api } from '../../services/api.js' vi.mock('../../services/api.js', () => ({ api: { getSetupWizardState: vi.fn(), - setSetupWizardStorage: vi.fn(), completeSetupWizard: vi.fn(), }, })) @@ -51,14 +50,9 @@ beforeEach(() => { data: { complete: false, currentRecommendedStep: 1, - contentStorage: 'database', - groupfolderAvailable: true, stepStatuses: { 1: 'done', 2: 'pending' }, }, }) - api.setSetupWizardStorage - .mockReset() - .mockResolvedValue({ data: { complete: false } }) api.completeSetupWizard .mockReset() .mockResolvedValue({ data: { complete: true } }) @@ -97,7 +91,7 @@ describe('SetupWizardModal', () => { expect(wrapper.find('[data-test="setup-wizard-counter"]').exists()).toBe( true, ) - expect(wrapper.vm.totalSteps).toBe(7) + expect(wrapper.vm.totalSteps).toBe(6) }) it('REQ-WIZ-002: Next advances; Back returns; counter updates', async () => { @@ -112,30 +106,26 @@ describe('SetupWizardModal', () => { expect(wrapper.vm.currentStep).toBe(1) }) - it('REQ-WIZ-003: Step 2 Next persists the storage choice via the API', async () => { + it('decision 131: Step 2 is the group order and no storage step renders', async () => { const wrapper = mountWizard() await flush() - // Advance to Step 2. await wrapper.find('[data-test="setup-wizard-next"]').trigger('click') await flush() expect(wrapper.vm.currentStep).toBe(2) - - // Change selection then click Next. - wrapper.vm.storage = 'groupfolder' - await wrapper.find('[data-test="setup-wizard-next"]').trigger('click') - await flush() - - expect(api.setSetupWizardStorage).toHaveBeenCalledWith('groupfolder') - expect(wrapper.vm.currentStep).toBe(3) + expect(wrapper.find('.group-priority-stub').exists()).toBe(true) + expect(wrapper.find('[data-test="storage-database"]').exists()).toBe(false) + expect(wrapper.find('[data-test="storage-groupfolder"]').exists()).toBe( + false, + ) }) - it('REQ-WIZ-002: Step 7 Next is labelled Finish and calls completeSetupWizard', async () => { + it('REQ-WIZ-002: Step 6 Next is labelled Finish and calls completeSetupWizard', async () => { const wrapper = mountWizard() await flush() - // Jump straight to step 7 to keep the test focused. - wrapper.vm.currentStep = 7 + // Jump straight to step 6 to keep the test focused. + wrapper.vm.currentStep = 6 await flush() // The Vue mixin's t() stub returns the bare key; the component @@ -150,27 +140,6 @@ describe('SetupWizardModal', () => { expect(wrapper.emitted('close')).toBeTruthy() }) - it('REQ-WIZ-003: GroupFolder radio is disabled when the app is missing', async () => { - api.getSetupWizardState.mockResolvedValueOnce({ - data: { - complete: false, - currentRecommendedStep: 1, - contentStorage: 'database', - groupfolderAvailable: false, - stepStatuses: { 1: 'done' }, - }, - }) - const wrapper = mountWizard() - await flush() - - // Advance to step 2. - await wrapper.find('[data-test="setup-wizard-next"]').trigger('click') - await flush() - - const radio = wrapper.find('[data-test="storage-groupfolder"]') - expect(radio.attributes('disabled')).toBeDefined() - }) - it('REQ-WIZ-002: Skip advances without committing on optional steps', async () => { const wrapper = mountWizard() await flush() @@ -179,6 +148,6 @@ describe('SetupWizardModal', () => { await wrapper.find('[data-test="setup-wizard-skip"]').trigger('click') expect(wrapper.vm.currentStep).toBe(5) - expect(api.setSetupWizardStorage).not.toHaveBeenCalled() + expect(api.completeSetupWizard).not.toHaveBeenCalled() }) }) diff --git a/src/services/api.js b/src/services/api.js index 05ddcb2f4..22eccc485 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -952,17 +952,6 @@ export const api = { return axios.post(`${baseUrl}/api/admin/setup-wizard/complete`) }, - /** - * Persist the setup wizard's content-storage choice (REQ-WIZ-003). - * - * @param {string} storage Backend to store widget content in — - * `database` or `groupfolder`. - * @return {Promise} Axios response resolving to the updated wizard state. - */ - setSetupWizardStorage(storage) { - return axios.post(`${baseUrl}/api/admin/setup-wizard/storage`, { storage }) - }, - /** * Export dashboards as a downloadable archive (REQ-EXIM-002..004). * Requires Nextcloud-admin, gated server-side; the admin UI renders the diff --git a/tests/Unit/Activity/Settings/ActivitySettingsTest.php b/tests/Unit/Activity/Settings/ActivitySettingsTest.php new file mode 100644 index 000000000..440b9b731 --- /dev/null +++ b/tests/Unit/Activity/Settings/ActivitySettingsTest.php @@ -0,0 +1,99 @@ + + * @copyright 2026 Conduction b.v. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 LaunchPad Contributors + * SPDX-License-Identifier: EUPL-1.2 + */ + +declare(strict_types=1); + +namespace OCA\LaunchPad\Tests\Unit\Activity\Settings; + +use OCA\LaunchPad\Activity\Extension; +use OCA\LaunchPad\Activity\Settings\DashboardAcknowledgedSetting; +use OCA\LaunchPad\Activity\Settings\DashboardPublishedSetting; +use OCA\LaunchPad\Activity\Settings\DashboardSharedSetting; +use OCA\LaunchPad\Activity\Settings\DashboardUpdatedSetting; +use OCP\Activity\ActivitySettings; +use OCP\IL10N; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; + +class ActivitySettingsTest extends TestCase { + /** + * @return array, string, bool}> + */ + public static function settings(): array { + return [ + 'shared' => [DashboardSharedSetting::class, Extension::EVENT_SHARED, true], + 'published' => [DashboardPublishedSetting::class, Extension::EVENT_PUBLISHED, true], + 'updated' => [DashboardUpdatedSetting::class, Extension::EVENT_UPDATED, false], + 'acknowledged' => [DashboardAcknowledgedSetting::class, Extension::EVENT_ACKNOWLEDGED, false], + ]; + } + + private function l10n(): IL10N { + $l = $this->createMock(IL10N::class); + $l->method('t')->willReturnCallback(static fn (string $text): string => $text); + return $l; + } + + /** + * @param class-string $class + */ + #[DataProvider('settings')] + public function testDefaultsAndGroup(string $class, string $type, bool $mail): void { + $setting = new $class($this->l10n()); + + $this->assertInstanceOf(ActivitySettings::class, $setting); + $this->assertSame($type, $setting->getIdentifier()); + $this->assertSame('launchpad', $setting->getGroupIdentifier()); + $this->assertSame('LaunchPad', $setting->getGroupName()); + $this->assertNotSame('', $setting->getName()); + $this->assertTrue($setting->canChangeStream()); + $this->assertTrue($setting->isDefaultEnabledStream()); + $this->assertTrue($setting->canChangeMail()); + $this->assertSame($mail, $setting->isDefaultEnabledMail()); + $this->assertGreaterThanOrEqual(0, $setting->getPriority()); + $this->assertLessThanOrEqual(100, $setting->getPriority()); + } + + public function testInfoXmlRegistersEverySetting(): void { + // Read the file ourselves: under the Nextcloud bootstrap (CI) the + // external entity loader is disabled, so simplexml_load_file() on a + // path returns false. Loading the string needs no entity loader. + $xml = simplexml_load_string((string)file_get_contents(__DIR__ . '/../../../../appinfo/info.xml')); + $this->assertNotFalse($xml); + $registered = []; + foreach ($xml->activity->settings->setting ?? [] as $node) { + $registered[] = (string)$node; + } + + $expected = array_map(static fn (array $row): string => $row[0], self::settings()); + sort($expected); + sort($registered); + $this->assertSame($expected, $registered); + foreach ($registered as $class) { + $this->assertTrue(class_exists($class), $class . ' is registered in info.xml but does not exist'); + } + } + + public function testIdentifiersAreInTheProviderCatalogue(): void { + foreach (self::settings() as [$class, $type]) { + $this->assertContains($type, Extension::ALL_EVENTS); + } + } +} diff --git a/tests/Unit/Command/SetupCommandTest.php b/tests/Unit/Command/SetupCommandTest.php new file mode 100644 index 000000000..64f56f210 --- /dev/null +++ b/tests/Unit/Command/SetupCommandTest.php @@ -0,0 +1,109 @@ + + * @copyright 2026 Conduction b.v. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 LaunchPad Contributors + * SPDX-License-Identifier: EUPL-1.2 + */ + +declare(strict_types=1); + +namespace OCA\LaunchPad\Tests\Unit\Command; + +use OCA\LaunchPad\Command\SetupCommand; +use OCA\LaunchPad\Service\AdminSettingsService; +use OCA\LaunchPad\Service\SetupWizardService; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Tester\CommandTester; + +class SetupCommandTest extends TestCase { + /** @var SetupWizardService&MockObject */ + private $wizard; + + /** @var AdminSettingsService&MockObject */ + private $settings; + + /** @var list */ + private array $files = []; + + protected function setUp(): void { + parent::setUp(); + $this->wizard = $this->createMock(SetupWizardService::class); + $this->settings = $this->createMock(AdminSettingsService::class); + } + + protected function tearDown(): void { + foreach ($this->files as $file) { + @unlink($file); + } + parent::tearDown(); + } + + private function yaml(string $body): string { + $file = tempnam(sys_get_temp_dir(), 'lp-setup-'); + $this->assertIsString($file); + file_put_contents($file, $body); + $this->files[] = $file; + return $file; + } + + private function runSetup(string $body): CommandTester { + $tester = new CommandTester(new SetupCommand($this->wizard, $this->settings)); + $tester->execute(['--config' => $this->yaml($body)]); + return $tester; + } + + public function testAConfigWithoutStorageBackendSucceeds(): void { + $this->settings->method('getGroupOrder')->willReturn([]); + $this->settings->expects($this->once())->method('setGroupOrder')->with(['engineering', 'sales']); + $this->wizard->expects($this->once())->method('markWizardComplete'); + + $tester = $this->runSetup("group_priority_order: [\"engineering\", \"sales\"]\n"); + + $this->assertSame(0, $tester->getStatusCode()); + $display = $tester->getDisplay(); + $this->assertStringContainsString('Step 1: Welcome... done', $display); + $this->assertStringContainsString('Step 2: Group order... done', $display); + $this->assertStringContainsString('Step 3: Demo data... skipped (not in config)', $display); + $this->assertStringContainsString('Step 4: Admin roles... skipped (not in config)', $display); + $this->assertStringContainsString('Step 5: Footer config... skipped (not in config)', $display); + $this->assertStringContainsString('Step 6: Done... done', $display); + $this->assertStringNotContainsString('Storage backend', $display); + $this->assertStringContainsString('Setup wizard completed successfully.', $display); + } + + public function testAnOldConfigWithStorageBackendStillRunsAndSaysTheFieldIsIgnored(): void { + $this->settings->method('getGroupOrder')->willReturn(['engineering']); + $this->settings->expects($this->never())->method('setGroupOrder'); + $this->wizard->expects($this->once())->method('markWizardComplete'); + + $tester = $this->runSetup("storage_backend: \"groupfolder\"\ngroup_priority_order: [\"engineering\"]\n"); + + $this->assertSame(0, $tester->getStatusCode()); + $display = $tester->getDisplay(); + $this->assertStringContainsString("'storage_backend' is no longer used and was ignored", $display); + $this->assertStringContainsString('Step 2: Group order... already configured, skipping', $display); + } + + public function testAnEmptyMapStillCompletesTheWizard(): void { + $this->wizard->expects($this->once())->method('markWizardComplete'); + + $tester = $this->runSetup("{}\n"); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('Step 2: Group order... skipped (not in config)', $tester->getDisplay()); + } +} diff --git a/tests/Unit/Controller/AdminControllerSettingsAliasTest.php b/tests/Unit/Controller/AdminControllerSettingsAliasTest.php index a1a58a52b..a453d9c91 100644 --- a/tests/Unit/Controller/AdminControllerSettingsAliasTest.php +++ b/tests/Unit/Controller/AdminControllerSettingsAliasTest.php @@ -177,7 +177,6 @@ public function testStartPageWithoutNavigationIsPassedThrough(): void { $this->isNull(), $this->isNull(), $this->isNull(), - $this->isNull(), $this->isTrue(), ); diff --git a/tests/Unit/Controller/AdminControllerSetupWizardTest.php b/tests/Unit/Controller/AdminControllerSetupWizardTest.php index 94c2a8afb..b4fd958cf 100644 --- a/tests/Unit/Controller/AdminControllerSetupWizardTest.php +++ b/tests/Unit/Controller/AdminControllerSetupWizardTest.php @@ -3,9 +3,9 @@ /** * AdminController Setup-Wizard Test * - * Covers the `getWizardState`, `completeWizard`, and `setWizardStorage` - * endpoints added by the `setup-wizard` change (REQ-WIZ-008, REQ-WIZ-009, - * REQ-WIZ-003). + * Covers the `getWizardState` and `completeWizard` endpoints added by the + * `setup-wizard` change (REQ-WIZ-008, REQ-WIZ-009), and pins that the + * retired storage-step endpoint stays gone (decision 131). * * @category Test * @package OCA\LaunchPad\Tests\Unit\Controller @@ -136,52 +136,13 @@ public function testCompleteWizardRejectsNonAdminWith403(): void { $this->assertSame(Http::STATUS_FORBIDDEN, $response->getStatus()); } - public function testSetWizardStorageRejectsEmptyValue(): void { - $this->loginAsAdmin(); - $this->wizardService->expects($this->never())->method('setContentStorage'); - - $response = $this->controller->setWizardStorage(storage: ''); - - $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus()); - } - - public function testSetWizardStorageRejectsGroupfolderWhenAppMissing(): void { - $this->loginAsAdmin(); - $this->wizardService->method('hasGroupfolderApp')->willReturn(false); - $this->wizardService->expects($this->never())->method('setContentStorage'); - - $response = $this->controller->setWizardStorage(storage: 'groupfolder'); - - $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus()); - } - - public function testSetWizardStorageWritesAndReturnsState(): void { - $this->loginAsAdmin(); - $this->wizardService->method('hasGroupfolderApp')->willReturn(true); - - $this->wizardService->expects($this->once()) - ->method('setContentStorage') - ->with('groupfolder'); - - $payload = [ - 'complete' => false, - 'currentRecommendedStep' => 3, - 'stepStatuses' => ['2' => 'done'], - ]; - $this->wizardService->method('getWizardState')->willReturn($payload); - - $response = $this->controller->setWizardStorage(storage: 'groupfolder'); - - $this->assertSame(Http::STATUS_OK, $response->getStatus()); - $this->assertSame($payload, $response->getData()); - } - - public function testSetWizardStorageReturns401WhenLoggedOut(): void { - $this->userSession->method('getUser')->willReturn(null); - $this->wizardService->expects($this->never())->method('setContentStorage'); - - $response = $this->controller->setWizardStorage(storage: 'database'); + public function testTheStorageStepEndpointIsRetired(): void { + // Decision 131: the wizard no longer asks where content is stored. + $this->assertFalse(method_exists(AdminController::class, 'setWizardStorage')); - $this->assertSame(Http::STATUS_UNAUTHORIZED, $response->getStatus()); + $routes = file_get_contents(__DIR__ . '/../../../appinfo/routes.php'); + $this->assertIsString($routes); + $this->assertStringNotContainsString('setup-wizard/storage', $routes); + $this->assertStringNotContainsString('setWizardStorage', $routes); } } diff --git a/tests/Unit/Db/AdminSettingKeyTest.php b/tests/Unit/Db/AdminSettingKeyTest.php index 1d8d601e7..dd6f9c3a9 100644 --- a/tests/Unit/Db/AdminSettingKeyTest.php +++ b/tests/Unit/Db/AdminSettingKeyTest.php @@ -87,7 +87,6 @@ public static function bcAliasProvider(): array { 'FOOTER_BACKGROUND_COLOR' => [AdminSetting::KEY_FOOTER_BACKGROUND_COLOR, AdminSettingKey::FOOTER_BACKGROUND_COLOR], 'FOOTER_TEXT_COLOR' => [AdminSetting::KEY_FOOTER_TEXT_COLOR, AdminSettingKey::FOOTER_TEXT_COLOR], 'SETUP_WIZARD_COMPLETE' => [AdminSetting::KEY_SETUP_WIZARD_COMPLETE, AdminSettingKey::SETUP_WIZARD_COMPLETE], - 'CONTENT_STORAGE' => [AdminSetting::KEY_CONTENT_STORAGE, AdminSettingKey::CONTENT_STORAGE], ]; }//end bcAliasProvider() @@ -111,7 +110,6 @@ public function testFromWorksForEveryAlias(): void { AdminSetting::KEY_FOOTER_BACKGROUND_COLOR, AdminSetting::KEY_FOOTER_TEXT_COLOR, AdminSetting::KEY_SETUP_WIZARD_COMPLETE, - AdminSetting::KEY_CONTENT_STORAGE, ]; foreach ($aliases as $alias) { @@ -120,4 +118,15 @@ public function testFromWorksForEveryAlias(): void { $this->assertSame(expected: $alias, actual: $case->value); } }//end testFromWorksForEveryAlias() + + /** + * Decision 131: the content storage key is retired; a stored row is + * left unread, so the enum must not resolve it. + * + * @return void + */ + public function testContentStorageKeyIsRetired(): void { + $this->assertNull(AdminSettingKey::tryFrom('content_storage')); + $this->assertFalse(defined(AdminSetting::class . '::KEY_CONTENT_STORAGE')); + }//end testContentStorageKeyIsRetired() }//end class diff --git a/tests/Unit/Service/AdminSettingsServiceTest.php b/tests/Unit/Service/AdminSettingsServiceTest.php index 131e203d3..7b9fbb19e 100644 --- a/tests/Unit/Service/AdminSettingsServiceTest.php +++ b/tests/Unit/Service/AdminSettingsServiceTest.php @@ -72,7 +72,6 @@ public function testGetSettingsReturnsExactlyTheDocumentedKeysAtTheirDefaults(): 'allowMultipleDashboards' => true, 'defaultGridColumns' => 12, 'linkCreateFileExtensions' => ['txt', 'md', 'docx', 'xlsx', 'csv', 'odt'], - 'launchpad.content_storage' => 'database', 'defaultSharePermissionLevel' => 'add_only', 'forcedShareGroups' => [], 'legacyWidgetBridgeEnabled' => false, @@ -155,8 +154,8 @@ public function testGetSettingsReturnsCamelCaseKeys(): void { $this->assertArrayHasKey('allowMultipleDashboards', $settings); $this->assertArrayHasKey('defaultGridColumns', $settings); $this->assertArrayHasKey('linkCreateFileExtensions', $settings); - // REQ-GFSB-006: content storage backend key added. - $this->assertArrayHasKey('launchpad.content_storage', $settings); + // Decision 131: the content storage key is retired. + $this->assertArrayNotHasKey('launchpad.content_storage', $settings); // dashboard-sharing + legacy-widget-bridge spec keys. $this->assertArrayHasKey('defaultSharePermissionLevel', $settings); $this->assertArrayHasKey('forcedShareGroups', $settings); @@ -168,7 +167,7 @@ public function testGetSettingsReturnsCamelCaseKeys(): void { $this->assertArrayHasKey('quicksearchFallbackTarget', $settings); // runtime-shell REQ-SHELL-009. $this->assertArrayHasKey('startPageWithoutNavigation', $settings); - $this->assertCount(13, $settings); + $this->assertCount(12, $settings); }//end testGetSettingsReturnsCamelCaseKeys() // ----- dashboard-quota-limits REQ-QUOTA-001 ----- diff --git a/tests/Unit/Service/SetupWizardServiceTest.php b/tests/Unit/Service/SetupWizardServiceTest.php index ce7840d91..0b3f93f05 100644 --- a/tests/Unit/Service/SetupWizardServiceTest.php +++ b/tests/Unit/Service/SetupWizardServiceTest.php @@ -3,9 +3,9 @@ /** * SetupWizardService Test * - * Covers REQ-WIZ-001 (state flag), REQ-WIZ-003 (storage backend - * persistence + GroupFolder gate), REQ-WIZ-008 (wizard state heuristic), - * and REQ-WIZ-009 (idempotent completion). + * Covers REQ-WIZ-001 (state flag), REQ-WIZ-008 (wizard state heuristic), + * REQ-WIZ-009 (idempotent completion) and the retired storage step + * (decision 131: six steps, a stored content_storage value stays unread). * * @category Test * @package OCA\LaunchPad\Tests\Unit\Service @@ -21,11 +21,9 @@ namespace Unit\Service; -use InvalidArgumentException; use OCA\LaunchPad\Db\AdminSetting; use OCA\LaunchPad\Db\AdminSettingMapper; use OCA\LaunchPad\Service\SetupWizardService; -use OCP\App\IAppManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -35,15 +33,10 @@ class SetupWizardServiceTest extends TestCase { /** @var AdminSettingMapper&MockObject */ private $settingMapper; - /** @var IAppManager&MockObject */ - private $appManager; - protected function setUp(): void { $this->settingMapper = $this->createMock(AdminSettingMapper::class); - $this->appManager = $this->createMock(IAppManager::class); $this->service = new SetupWizardService( - settingMapper: $this->settingMapper, - appManager: $this->appManager + settingMapper: $this->settingMapper ); } @@ -56,28 +49,59 @@ public function testGetWizardStateOnFreshInstance(): void { $state = $this->service->getWizardState(); $this->assertFalse($state['complete']); + // Decision 131: the storage step is retired, so Step 2 is the + // group order and it is the first step left to do. + $this->assertSame(2, $state['currentRecommendedStep']); + $this->assertSame( + [ + 1 => 'done', + 2 => 'pending', + 3 => 'skipped', + 4 => 'skipped', + 5 => 'skipped', + 6 => 'pending', + ], + $state['stepStatuses'] + ); + } + + public function testWizardHasSixStepsWithoutAStorageStep(): void { + $this->assertSame(6, SetupWizardService::STEP_COUNT); + $this->assertFalse(method_exists(SetupWizardService::class, 'setContentStorage')); + $this->assertFalse(method_exists(SetupWizardService::class, 'getContentStorage')); + $this->assertFalse(method_exists(SetupWizardService::class, 'hasGroupfolderApp')); + } + + public function testAStoredContentStorageValueIsLeftUnread(): void { + $this->settingMapper->method('getValue') + ->with(AdminSetting::KEY_SETUP_WIZARD_COMPLETE, false) + ->willReturn(false); + // A row written by the retired step stays in the table; the state + // must not change because of it. + $this->settingMapper->method('getAllAsArray')->willReturn([ + 'content_storage' => 'groupfolder', + ]); + + $state = $this->service->getWizardState(); + $this->assertSame(2, $state['currentRecommendedStep']); - $this->assertSame('done', $state['stepStatuses']['1']); + $this->assertCount(6, $state['stepStatuses']); $this->assertSame('pending', $state['stepStatuses']['2']); - $this->assertSame('pending', $state['stepStatuses']['3']); - $this->assertSame('skipped', $state['stepStatuses']['4']); - $this->assertSame('skipped', $state['stepStatuses']['5']); - $this->assertSame('skipped', $state['stepStatuses']['6']); - $this->assertSame('pending', $state['stepStatuses']['7']); } - public function testGetWizardStateAfterStorageWritten(): void { + public function testGetWizardStateAfterGroupOrderWritten(): void { $this->settingMapper->method('getValue') ->with(AdminSetting::KEY_SETUP_WIZARD_COMPLETE, false) ->willReturn(false); $this->settingMapper->method('getAllAsArray')->willReturn([ - AdminSetting::KEY_CONTENT_STORAGE => 'database', + AdminSetting::KEY_GROUP_ORDER => ['engineering'], ]); $state = $this->service->getWizardState(); $this->assertSame('done', $state['stepStatuses']['2']); - // Step 3 still pending so the recommended step jumps to 3. + // Steps 3 and 4 are 'skipped' (sibling capabilities pending), so + // the first non-'done' step is 3. $this->assertSame(3, $state['currentRecommendedStep']); } @@ -86,7 +110,6 @@ public function testGetWizardStateAfterCompletion(): void { ->with(AdminSetting::KEY_SETUP_WIZARD_COMPLETE, false) ->willReturn(true); $this->settingMapper->method('getAllAsArray')->willReturn([ - AdminSetting::KEY_CONTENT_STORAGE => 'database', AdminSetting::KEY_GROUP_ORDER => ['engineering'], AdminSetting::KEY_FOOTER_CONFIG => ['layout' => 'structured'], ]); @@ -94,13 +117,13 @@ public function testGetWizardStateAfterCompletion(): void { $state = $this->service->getWizardState(); $this->assertTrue($state['complete']); - $this->assertSame('done', $state['stepStatuses']['7']); $this->assertSame('done', $state['stepStatuses']['6']); - // Steps 4/5 are 'skipped' (sibling capabilities pending), so the - // first non-'done' status is Step 4. The wizard "complete" flag + $this->assertSame('done', $state['stepStatuses']['5']); + // Steps 3/4 are 'skipped' (sibling capabilities pending), so the + // first non-'done' status is Step 3. The wizard "complete" flag // is the source of truth for hiding the banner; the recommended // step is purely a UX hint per REQ-WIZ-008. - $this->assertSame(4, $state['currentRecommendedStep']); + $this->assertSame(3, $state['currentRecommendedStep']); } public function testMarkWizardCompleteSetsFlagAndReturnsState(): void { @@ -132,46 +155,4 @@ public function testMarkWizardCompleteIsIdempotent(): void { $this->assertSame($first, $second); } - - public function testGetGroupfolderAvailabilityDelegates(): void { - $this->appManager - ->expects($this->once()) - ->method('isInstalled') - ->with('groupfolders') - ->willReturn(true); - - $this->assertTrue($this->service->hasGroupfolderApp()); - } - - public function testSetContentStorageRejectsUnsupportedValue(): void { - $this->settingMapper->expects($this->never())->method('setSetting'); - $this->expectException(InvalidArgumentException::class); - - $this->service->setContentStorage(value: 'cassette-tape'); - } - - public function testSetContentStoragePersistsKnownValues(): void { - $this->settingMapper - ->expects($this->once()) - ->method('setSetting') - ->with(AdminSetting::KEY_CONTENT_STORAGE, 'groupfolder'); - - $this->service->setContentStorage(value: SetupWizardService::STORAGE_GROUPFOLDER); - } - - public function testGetContentStorageDefaultsToDatabase(): void { - $this->settingMapper->method('getValue') - ->with(AdminSetting::KEY_CONTENT_STORAGE, null) - ->willReturn(null); - - $this->assertSame('database', $this->service->getContentStorage()); - } - - public function testGetContentStorageReturnsPersisted(): void { - $this->settingMapper->method('getValue') - ->with(AdminSetting::KEY_CONTENT_STORAGE, null) - ->willReturn('groupfolder'); - - $this->assertSame('groupfolder', $this->service->getContentStorage()); - } } diff --git a/tests/integration/launchpad.postman_collection.json b/tests/integration/launchpad.postman_collection.json index cd18e12bb..1f5877a50 100644 --- a/tests/integration/launchpad.postman_collection.json +++ b/tests/integration/launchpad.postman_collection.json @@ -4818,48 +4818,6 @@ } ] }, - { - "name": "POST /api/admin/setup-wizard/storage", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"storage\":\"openregister\"}" - }, - "url": { - "raw": "{{baseUrl}}/index.php/apps/launchpad/api/admin/setup-wizard/storage", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "index.php", - "apps", - "launchpad", - "api", - "admin", - "setup-wizard", - "storage" - ] - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('2xx or 4xx', () => pm.expect([200, 204, 400, 403]).to.include(pm.response.code));" - ] - } - } - ] - }, { "name": "POST /api/admin/setup-wizard/complete", "request": { diff --git a/tools/spec-annotations-allowlist.txt b/tools/spec-annotations-allowlist.txt index b772a7cf2..03085f434 100644 --- a/tools/spec-annotations-allowlist.txt +++ b/tools/spec-annotations-allowlist.txt @@ -39,7 +39,6 @@ OCA\LaunchPad\Controller\AdminController::refreshFeedsNow OCA\LaunchPad\Controller\AdminController::uploadTemplatePreviewImage OCA\LaunchPad\Controller\AdminController::getWizardState OCA\LaunchPad\Controller\AdminController::completeWizard -OCA\LaunchPad\Controller\AdminController::setWizardStorage OCA\LaunchPad\Controller\AdminDemoShowcasesController::index OCA\LaunchPad\Controller\AdminDemoShowcasesController::install OCA\LaunchPad\Controller\AdminDemoShowcasesController::destroy @@ -671,9 +670,6 @@ OCA\LaunchPad\Service\RoleService::isViewer OCA\LaunchPad\Service\RoleService::canMutate OCA\LaunchPad\Service\SetupWizardService::getWizardState OCA\LaunchPad\Service\SetupWizardService::markWizardComplete -OCA\LaunchPad\Service\SetupWizardService::hasGroupfolderApp -OCA\LaunchPad\Service\SetupWizardService::setContentStorage -OCA\LaunchPad\Service\SetupWizardService::getContentStorage OCA\LaunchPad\Service\SvgSanitiser::sanitize OCA\LaunchPad\Service\TileUpdater::applyTileConfig OCA\LaunchPad\Service\TileUpdater::applyTileUpdates