DeclarativeManagerTest.php 19 KB

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