1
0

DeclarativeManagerTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace Test\Settings;
  8. use OC\AppFramework\Bootstrap\Coordinator;
  9. use OC\Settings\DeclarativeManager;
  10. use OCP\EventDispatcher\IEventDispatcher;
  11. use OCP\IAppConfig;
  12. use OCP\IConfig;
  13. use OCP\IGroupManager;
  14. use OCP\IUser;
  15. use OCP\Settings\DeclarativeSettingsTypes;
  16. use OCP\Settings\Events\DeclarativeSettingsSetValueEvent;
  17. use OCP\Settings\IDeclarativeManager;
  18. use PHPUnit\Framework\MockObject\MockObject;
  19. use Psr\Log\LoggerInterface;
  20. use Test\TestCase;
  21. class DeclarativeManagerTest extends TestCase {
  22. /** @var IDeclarativeManager|MockObject */
  23. private $declarativeManager;
  24. /** @var IEventDispatcher|MockObject */
  25. private $eventDispatcher;
  26. /** @var IGroupManager|MockObject */
  27. private $groupManager;
  28. /** @var Coordinator|MockObject */
  29. private $coordinator;
  30. /** @var IConfig|MockObject */
  31. private $config;
  32. /** @var IAppConfig|MockObject */
  33. private $appConfig;
  34. /** @var LoggerInterface|MockObject */
  35. private $logger;
  36. /** @var IUser|MockObject */
  37. private $user;
  38. /** @var IUser|MockObject */
  39. private $adminUser;
  40. public const validSchemaAllFields = [
  41. 'id' => 'test_form_1',
  42. 'priority' => 10,
  43. 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, // admin, personal
  44. 'section_id' => 'additional',
  45. 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, // external, internal (handled by core to store in appconfig and preferences)
  46. 'title' => 'Test declarative settings', // NcSettingsSection name
  47. 'description' => 'These fields are rendered dynamically from declarative schema', // NcSettingsSection description
  48. 'doc_url' => '', // NcSettingsSection doc_url for documentation or help page, empty string if not needed
  49. 'fields' => [
  50. [
  51. 'id' => 'test_field_7', // configkey
  52. 'title' => 'Multi-selection', // name or label
  53. 'description' => 'Select some option setting', // hint
  54. 'type' => DeclarativeSettingsTypes::MULTI_SELECT,
  55. 'options' => ['foo', 'bar', 'baz'], // simple options for select, radio, multi-select
  56. 'placeholder' => 'Select some multiple options', // input placeholder
  57. 'default' => ['foo', 'bar'],
  58. ],
  59. [
  60. 'id' => 'some_real_setting',
  61. 'title' => 'Select single option',
  62. 'description' => 'Single option radio buttons',
  63. 'type' => DeclarativeSettingsTypes::RADIO, // radio (NcCheckboxRadioSwitch type radio)
  64. 'placeholder' => 'Select single option, test interval',
  65. 'default' => '40m',
  66. 'options' => [
  67. [
  68. 'name' => 'Each 40 minutes', // NcCheckboxRadioSwitch display name
  69. 'value' => '40m' // NcCheckboxRadioSwitch value
  70. ],
  71. [
  72. 'name' => 'Each 60 minutes',
  73. 'value' => '60m'
  74. ],
  75. [
  76. 'name' => 'Each 120 minutes',
  77. 'value' => '120m'
  78. ],
  79. [
  80. 'name' => 'Each day',
  81. 'value' => 60 * 24 . 'm'
  82. ],
  83. ],
  84. ],
  85. [
  86. 'id' => 'test_field_1', // configkey
  87. 'title' => 'Default text field', // label
  88. 'description' => 'Set some simple text setting', // hint
  89. 'type' => DeclarativeSettingsTypes::TEXT,
  90. 'placeholder' => 'Enter text setting', // placeholder
  91. 'default' => 'foo',
  92. ],
  93. [
  94. 'id' => 'test_field_1_1',
  95. 'title' => 'Email field',
  96. 'description' => 'Set email config',
  97. 'type' => DeclarativeSettingsTypes::EMAIL,
  98. 'placeholder' => 'Enter email',
  99. 'default' => '',
  100. ],
  101. [
  102. 'id' => 'test_field_1_2',
  103. 'title' => 'Tel field',
  104. 'description' => 'Set tel config',
  105. 'type' => DeclarativeSettingsTypes::TEL,
  106. 'placeholder' => 'Enter your tel',
  107. 'default' => '',
  108. ],
  109. [
  110. 'id' => 'test_field_1_3',
  111. 'title' => 'Url (website) field',
  112. 'description' => 'Set url config',
  113. 'type' => 'url',
  114. 'placeholder' => 'Enter url',
  115. 'default' => '',
  116. ],
  117. [
  118. 'id' => 'test_field_1_4',
  119. 'title' => 'Number field',
  120. 'description' => 'Set number config',
  121. 'type' => DeclarativeSettingsTypes::NUMBER,
  122. 'placeholder' => 'Enter number value',
  123. 'default' => 0,
  124. ],
  125. [
  126. 'id' => 'test_field_2',
  127. 'title' => 'Password',
  128. 'description' => 'Set some secure value setting',
  129. 'type' => 'password',
  130. 'placeholder' => 'Set secure value',
  131. 'default' => '',
  132. ],
  133. [
  134. 'id' => 'test_field_3',
  135. 'title' => 'Selection',
  136. 'description' => 'Select some option setting',
  137. 'type' => DeclarativeSettingsTypes::SELECT,
  138. 'options' => ['foo', 'bar', 'baz'],
  139. 'placeholder' => 'Select some option setting',
  140. 'default' => 'foo',
  141. ],
  142. [
  143. 'id' => 'test_field_4',
  144. 'title' => 'Toggle something',
  145. 'description' => 'Select checkbox option setting',
  146. 'type' => DeclarativeSettingsTypes::CHECKBOX,
  147. 'label' => 'Verify something if enabled',
  148. 'default' => false,
  149. ],
  150. [
  151. 'id' => 'test_field_5',
  152. 'title' => 'Multiple checkbox toggles, describing one setting, checked options are saved as an JSON object {foo: true, bar: false}',
  153. 'description' => 'Select checkbox option setting',
  154. 'type' => DeclarativeSettingsTypes::MULTI_CHECKBOX,
  155. 'default' => ['foo' => true, 'bar' => true],
  156. 'options' => [
  157. [
  158. 'name' => 'Foo',
  159. 'value' => 'foo', // multiple-checkbox configkey
  160. ],
  161. [
  162. 'name' => 'Bar',
  163. 'value' => 'bar',
  164. ],
  165. [
  166. 'name' => 'Baz',
  167. 'value' => 'baz',
  168. ],
  169. [
  170. 'name' => 'Qux',
  171. 'value' => 'qux',
  172. ],
  173. ],
  174. ],
  175. [
  176. 'id' => 'test_field_6',
  177. 'title' => 'Radio toggles, describing one setting like single select',
  178. 'description' => 'Select radio option setting',
  179. 'type' => DeclarativeSettingsTypes::RADIO, // radio (NcCheckboxRadioSwitch type radio)
  180. 'label' => 'Select single toggle',
  181. 'default' => 'foo',
  182. 'options' => [
  183. [
  184. 'name' => 'First radio', // NcCheckboxRadioSwitch display name
  185. 'value' => 'foo' // NcCheckboxRadioSwitch value
  186. ],
  187. [
  188. 'name' => 'Second radio',
  189. 'value' => 'bar'
  190. ],
  191. [
  192. 'name' => 'Second radio',
  193. 'value' => 'baz'
  194. ],
  195. ],
  196. ],
  197. ],
  198. ];
  199. public static bool $testSetInternalValueAfterChange = false;
  200. protected function setUp(): void {
  201. parent::setUp();
  202. $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
  203. $this->groupManager = $this->createMock(IGroupManager::class);
  204. $this->coordinator = $this->createMock(Coordinator::class);
  205. $this->config = $this->createMock(IConfig::class);
  206. $this->appConfig = $this->createMock(IAppConfig::class);
  207. $this->logger = $this->createMock(LoggerInterface::class);
  208. $this->declarativeManager = new DeclarativeManager(
  209. $this->eventDispatcher,
  210. $this->groupManager,
  211. $this->coordinator,
  212. $this->config,
  213. $this->appConfig,
  214. $this->logger
  215. );
  216. $this->user = $this->createMock(IUser::class);
  217. $this->user->expects($this->any())
  218. ->method('getUID')
  219. ->willReturn('test_user');
  220. $this->adminUser = $this->createMock(IUser::class);
  221. $this->adminUser->expects($this->any())
  222. ->method('getUID')
  223. ->willReturn('admin_test_user');
  224. $this->groupManager->expects($this->any())
  225. ->method('isAdmin')
  226. ->willReturnCallback(function ($userId) {
  227. return $userId === 'admin_test_user';
  228. });
  229. }
  230. public function testRegisterSchema(): void {
  231. $app = 'testing';
  232. $schema = self::validSchemaAllFields;
  233. $this->declarativeManager->registerSchema($app, $schema);
  234. $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']);
  235. $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app]));
  236. }
  237. /**
  238. * Simple test to verify that exception is thrown when trying to register schema with duplicate id
  239. */
  240. public function testRegisterDuplicateSchema(): void {
  241. $this->declarativeManager->registerSchema('testing', self::validSchemaAllFields);
  242. $this->expectException(\Exception::class);
  243. $this->declarativeManager->registerSchema('testing', self::validSchemaAllFields);
  244. }
  245. /**
  246. * It's not allowed to register schema with duplicate fields ids for the same app
  247. */
  248. public function testRegisterSchemaWithDuplicateFields(): void {
  249. // Register first valid schema
  250. $this->declarativeManager->registerSchema('testing', self::validSchemaAllFields);
  251. // Register second schema with duplicate fields, but different schema id
  252. $this->expectException(\Exception::class);
  253. $schema = self::validSchemaAllFields;
  254. $schema['id'] = 'test_form_2';
  255. $this->declarativeManager->registerSchema('testing', $schema);
  256. }
  257. public function testRegisterMultipleSchemasAndDuplicate(): void {
  258. $app = 'testing';
  259. $schema = self::validSchemaAllFields;
  260. $this->declarativeManager->registerSchema($app, $schema);
  261. $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']);
  262. // 1. Check that form is registered for the app
  263. $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app]));
  264. $app = 'testing2';
  265. $this->declarativeManager->registerSchema($app, $schema);
  266. $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']);
  267. // 2. Check that form is registered for the second app
  268. $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app]));
  269. $app = 'testing';
  270. $this->expectException(\Exception::class); // expecting duplicate form id and duplicate fields ids exception
  271. $this->declarativeManager->registerSchema($app, $schema);
  272. $schemaDuplicateFields = self::validSchemaAllFields;
  273. $schemaDuplicateFields['id'] = 'test_form_2'; // change form id to test duplicate fields
  274. $this->declarativeManager->registerSchema($app, $schemaDuplicateFields);
  275. // 3. Check that not valid form with duplicate fields is not registered
  276. $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schemaDuplicateFields['section_type'], $schemaDuplicateFields['section_id']);
  277. $this->assertFalse(isset($formIds[$app]) && in_array($schemaDuplicateFields['id'], $formIds[$app]));
  278. }
  279. /**
  280. * @dataProvider dataValidateSchema
  281. */
  282. public function testValidateSchema(bool $expected, bool $expectException, string $app, array $schema): void {
  283. if ($expectException) {
  284. $this->expectException(\Exception::class);
  285. }
  286. $this->declarativeManager->registerSchema($app, $schema);
  287. $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']);
  288. $this->assertEquals($expected, isset($formIds[$app]) && in_array($schema['id'], $formIds[$app]));
  289. }
  290. public static function dataValidateSchema(): array {
  291. return [
  292. 'valid schema with all supported fields' => [
  293. true,
  294. false,
  295. 'testing',
  296. self::validSchemaAllFields,
  297. ],
  298. 'invalid schema with missing id' => [
  299. false,
  300. true,
  301. 'testing',
  302. [
  303. 'priority' => 10,
  304. 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN,
  305. 'section_id' => 'additional',
  306. 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL,
  307. 'title' => 'Test declarative settings',
  308. 'description' => 'These fields are rendered dynamically from declarative schema',
  309. 'doc_url' => '',
  310. 'fields' => [
  311. [
  312. 'id' => 'test_field_7',
  313. 'title' => 'Multi-selection',
  314. 'description' => 'Select some option setting',
  315. 'type' => DeclarativeSettingsTypes::MULTI_SELECT,
  316. 'options' => ['foo', 'bar', 'baz'],
  317. 'placeholder' => 'Select some multiple options',
  318. 'default' => ['foo', 'bar'],
  319. ],
  320. ],
  321. ],
  322. ],
  323. 'invalid schema with invalid field' => [
  324. false,
  325. true,
  326. 'testing',
  327. [
  328. 'id' => 'test_form_1',
  329. 'priority' => 10,
  330. 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN,
  331. 'section_id' => 'additional',
  332. 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL,
  333. 'title' => 'Test declarative settings',
  334. 'description' => 'These fields are rendered dynamically from declarative schema',
  335. 'doc_url' => '',
  336. 'fields' => [
  337. [
  338. 'id' => 'test_invalid_field',
  339. 'title' => 'Invalid field',
  340. 'description' => 'Some invalid setting description',
  341. 'type' => 'some_invalid_type',
  342. 'placeholder' => 'Some invalid field placeholder',
  343. 'default' => null,
  344. ],
  345. ],
  346. ],
  347. ],
  348. ];
  349. }
  350. public function testGetFormIDs(): void {
  351. $app = 'testing';
  352. $schema = self::validSchemaAllFields;
  353. $this->declarativeManager->registerSchema($app, $schema);
  354. $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']);
  355. $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app]));
  356. $app = 'testing2';
  357. $this->declarativeManager->registerSchema($app, $schema);
  358. $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']);
  359. $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app]));
  360. }
  361. /**
  362. * Check that form with default values is returned with internal storage_type
  363. */
  364. public function testGetFormsWithDefaultValues(): void {
  365. $app = 'testing';
  366. $schema = self::validSchemaAllFields;
  367. $this->declarativeManager->registerSchema($app, $schema);
  368. $this->config->expects($this->any())
  369. ->method('getAppValue')
  370. ->willReturnCallback(fn ($app, $configkey, $default) => $default);
  371. $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']);
  372. $this->assertNotEmpty($forms);
  373. $this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false);
  374. // Check some_real_setting field default value
  375. $someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0];
  376. $schemaSomeRealSettingField = array_values(array_filter($schema['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0];
  377. $this->assertEquals($schemaSomeRealSettingField['default'], $someRealSettingField['default']);
  378. }
  379. /**
  380. * Check values in json format to ensure that they are properly encoded
  381. */
  382. public function testGetFormsWithDefaultValuesJson(): void {
  383. $app = 'testing';
  384. $schema = [
  385. 'id' => 'test_form_1',
  386. 'priority' => 10,
  387. 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL,
  388. 'section_id' => 'additional',
  389. 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL,
  390. 'title' => 'Test declarative settings',
  391. 'description' => 'These fields are rendered dynamically from declarative schema',
  392. 'doc_url' => '',
  393. 'fields' => [
  394. [
  395. 'id' => 'test_field_json',
  396. 'title' => 'Multi-selection',
  397. 'description' => 'Select some option setting',
  398. 'type' => DeclarativeSettingsTypes::MULTI_SELECT,
  399. 'options' => ['foo', 'bar', 'baz'],
  400. 'placeholder' => 'Select some multiple options',
  401. 'default' => ['foo', 'bar'],
  402. ],
  403. ],
  404. ];
  405. $this->declarativeManager->registerSchema($app, $schema);
  406. // config->getUserValue() should be called with json encoded default value
  407. $this->config->expects($this->once())
  408. ->method('getUserValue')
  409. ->with($this->adminUser->getUID(), $app, 'test_field_json', json_encode($schema['fields'][0]['default']))
  410. ->willReturn(json_encode($schema['fields'][0]['default']));
  411. $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']);
  412. $this->assertNotEmpty($forms);
  413. $this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false);
  414. $testFieldJson = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'test_field_json'))[0];
  415. $this->assertEquals(json_encode($schema['fields'][0]['default']), $testFieldJson['value']);
  416. }
  417. /**
  418. * Check that saving value for field with internal storage_type is handled by core
  419. */
  420. public function testSetInternalValue(): void {
  421. $app = 'testing';
  422. $schema = self::validSchemaAllFields;
  423. $this->declarativeManager->registerSchema($app, $schema);
  424. self::$testSetInternalValueAfterChange = false;
  425. $this->config->expects($this->any())
  426. ->method('getAppValue')
  427. ->willReturnCallback(function ($app, $configkey, $default) {
  428. if ($configkey === 'some_real_setting' && self::$testSetInternalValueAfterChange) {
  429. return '120m';
  430. }
  431. return $default;
  432. });
  433. $this->appConfig->expects($this->once())
  434. ->method('setValueString')
  435. ->with($app, 'some_real_setting', '120m');
  436. $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']);
  437. $someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0];
  438. $this->assertEquals('40m', $someRealSettingField['value']); // first check that default value (40m) is returned
  439. // Set new value for some_real_setting field
  440. $this->declarativeManager->setValue($this->adminUser, $app, $schema['id'], 'some_real_setting', '120m');
  441. self::$testSetInternalValueAfterChange = true;
  442. $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']);
  443. $this->assertNotEmpty($forms);
  444. $this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false);
  445. // Check some_real_setting field default value
  446. $someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0];
  447. $this->assertEquals('120m', $someRealSettingField['value']);
  448. }
  449. public function testSetExternalValue(): void {
  450. $app = 'testing';
  451. $schema = self::validSchemaAllFields;
  452. // Change storage_type to external and section_type to personal
  453. $schema['storage_type'] = DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL;
  454. $schema['section_type'] = DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL;
  455. $this->declarativeManager->registerSchema($app, $schema);
  456. $setDeclarativeSettingsValueEvent = new DeclarativeSettingsSetValueEvent(
  457. $this->adminUser,
  458. $app,
  459. $schema['id'],
  460. 'some_real_setting',
  461. '120m'
  462. );
  463. $this->eventDispatcher->expects($this->once())
  464. ->method('dispatchTyped')
  465. ->with($setDeclarativeSettingsValueEvent);
  466. $this->declarativeManager->setValue($this->adminUser, $app, $schema['id'], 'some_real_setting', '120m');
  467. }
  468. public function testAdminFormUserUnauthorized(): void {
  469. $app = 'testing';
  470. $schema = self::validSchemaAllFields;
  471. $this->declarativeManager->registerSchema($app, $schema);
  472. $this->expectException(\Exception::class);
  473. $this->declarativeManager->getFormsWithValues($this->user, $schema['section_type'], $schema['section_id']);
  474. }
  475. }