AccountManagerTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. <?php
  2. /**
  3. * @author Björn Schießle <schiessle@owncloud.com>
  4. *
  5. * @copyright Copyright (c) 2016, ownCloud, Inc.
  6. * @license AGPL-3.0
  7. *
  8. * This code is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License, version 3,
  10. * as published by the Free Software Foundation.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Affero General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License, version 3,
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>
  19. *
  20. */
  21. namespace Test\Accounts;
  22. use OC\Accounts\Account;
  23. use OC\Accounts\AccountManager;
  24. use OCP\Accounts\IAccountManager;
  25. use OCP\BackgroundJob\IJobList;
  26. use OCP\IConfig;
  27. use OCP\IDBConnection;
  28. use OCP\IUser;
  29. use PHPUnit\Framework\MockObject\MockObject;
  30. use Psr\Log\LoggerInterface;
  31. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  32. use Symfony\Component\EventDispatcher\GenericEvent;
  33. use Test\TestCase;
  34. /**
  35. * Class AccountManagerTest
  36. *
  37. * @group DB
  38. * @package Test\Accounts
  39. */
  40. class AccountManagerTest extends TestCase {
  41. /** @var \OCP\IDBConnection */
  42. private $connection;
  43. /** @var IConfig|MockObject */
  44. private $config;
  45. /** @var EventDispatcherInterface|MockObject */
  46. private $eventDispatcher;
  47. /** @var IJobList|MockObject */
  48. private $jobList;
  49. /** @var string accounts table name */
  50. private $table = 'accounts';
  51. /** @var LoggerInterface|MockObject */
  52. private $logger;
  53. /** @var AccountManager */
  54. private $accountManager;
  55. protected function setUp(): void {
  56. parent::setUp();
  57. $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
  58. $this->connection = \OC::$server->get(IDBConnection::class);
  59. $this->config = $this->createMock(IConfig::class);
  60. $this->jobList = $this->createMock(IJobList::class);
  61. $this->logger = $this->createMock(LoggerInterface::class);
  62. $this->accountManager = new AccountManager(
  63. $this->connection,
  64. $this->config,
  65. $this->eventDispatcher,
  66. $this->jobList,
  67. $this->logger,
  68. );
  69. }
  70. protected function tearDown(): void {
  71. parent::tearDown();
  72. $query = $this->connection->getQueryBuilder();
  73. $query->delete($this->table)->execute();
  74. }
  75. protected function makeUser(string $uid, string $name, string $email = null): IUser {
  76. $user = $this->createMock(IUser::class);
  77. $user->expects($this->any())
  78. ->method('getUid')
  79. ->willReturn($uid);
  80. $user->expects($this->any())
  81. ->method('getDisplayName')
  82. ->willReturn($name);
  83. if ($email !== null) {
  84. $user->expects($this->any())
  85. ->method('getEMailAddress')
  86. ->willReturn($email);
  87. }
  88. return $user;
  89. }
  90. protected function populateOrUpdate(): void {
  91. $users = [
  92. [
  93. 'user' => $this->makeUser('j.doe', 'Jane Doe', 'jane.doe@acme.com'),
  94. 'data' => [
  95. [
  96. 'name' => IAccountManager::PROPERTY_DISPLAYNAME,
  97. 'value' => 'Jane Doe',
  98. 'scope' => IAccountManager::SCOPE_PUBLISHED
  99. ],
  100. [
  101. 'name' => IAccountManager::PROPERTY_EMAIL,
  102. 'value' => 'jane.doe@acme.com',
  103. 'scope' => IAccountManager::SCOPE_LOCAL
  104. ],
  105. [
  106. 'name' => IAccountManager::PROPERTY_TWITTER,
  107. 'value' => '@sometwitter',
  108. 'scope' => IAccountManager::SCOPE_PUBLISHED
  109. ],
  110. [
  111. 'name' => IAccountManager::PROPERTY_PHONE,
  112. 'value' => '+491601231212',
  113. 'scope' => IAccountManager::SCOPE_FEDERATED
  114. ],
  115. [
  116. 'name' => IAccountManager::PROPERTY_ADDRESS,
  117. 'value' => 'some street',
  118. 'scope' => IAccountManager::SCOPE_LOCAL
  119. ],
  120. [
  121. 'name' => IAccountManager::PROPERTY_WEBSITE,
  122. 'value' => 'https://acme.com',
  123. 'scope' => IAccountManager::SCOPE_PRIVATE
  124. ],
  125. ],
  126. ],
  127. [
  128. 'user' => $this->makeUser('a.allison', 'Alice Allison', 'a.allison@example.org'),
  129. 'data' => [
  130. [
  131. 'name' => IAccountManager::PROPERTY_DISPLAYNAME,
  132. 'value' => 'Alice Allison',
  133. 'scope' => IAccountManager::SCOPE_LOCAL
  134. ],
  135. [
  136. 'name' => IAccountManager::PROPERTY_EMAIL,
  137. 'value' => 'a.allison@example.org',
  138. 'scope' => IAccountManager::SCOPE_LOCAL
  139. ],
  140. [
  141. 'name' => IAccountManager::PROPERTY_TWITTER,
  142. 'value' => '@a_alice',
  143. 'scope' => IAccountManager::SCOPE_FEDERATED
  144. ],
  145. [
  146. 'name' => IAccountManager::PROPERTY_PHONE,
  147. 'value' => '+491602312121',
  148. 'scope' => IAccountManager::SCOPE_LOCAL
  149. ],
  150. [
  151. 'name' => IAccountManager::PROPERTY_ADDRESS,
  152. 'value' => 'Dundee Road 45',
  153. 'scope' => IAccountManager::SCOPE_LOCAL
  154. ],
  155. [
  156. 'name' => IAccountManager::PROPERTY_WEBSITE,
  157. 'value' => 'https://example.org',
  158. 'scope' => IAccountManager::SCOPE_LOCAL
  159. ],
  160. ],
  161. ],
  162. [
  163. 'user' => $this->makeUser('b32c5a5b-1084-4380-8856-e5223b16de9f', 'Armel Oliseh', 'oliseh@example.com'),
  164. 'data' => [
  165. [
  166. 'name' => IAccountManager::PROPERTY_DISPLAYNAME,
  167. 'value' => 'Armel Oliseh',
  168. 'scope' => IAccountManager::SCOPE_PUBLISHED
  169. ],
  170. [
  171. 'name' => IAccountManager::PROPERTY_EMAIL,
  172. 'value' => 'oliseh@example.com',
  173. 'scope' => IAccountManager::SCOPE_PUBLISHED
  174. ],
  175. [
  176. 'name' => IAccountManager::PROPERTY_TWITTER,
  177. 'value' => '',
  178. 'scope' => IAccountManager::SCOPE_LOCAL
  179. ],
  180. [
  181. 'name' => IAccountManager::PROPERTY_PHONE,
  182. 'value' => '+491603121212',
  183. 'scope' => IAccountManager::SCOPE_PUBLISHED
  184. ],
  185. [
  186. 'name' => IAccountManager::PROPERTY_ADDRESS,
  187. 'value' => 'Sunflower Blvd. 77',
  188. 'scope' => IAccountManager::SCOPE_PUBLISHED
  189. ],
  190. [
  191. 'name' => IAccountManager::PROPERTY_WEBSITE,
  192. 'value' => 'https://example.com',
  193. 'scope' => IAccountManager::SCOPE_PUBLISHED
  194. ],
  195. ],
  196. ],
  197. [
  198. 'user' => $this->makeUser('31b5316a-9b57-4b17-970a-315a4cbe73eb', 'K. Cheng', 'cheng@emca.com'),
  199. 'data' => [
  200. [
  201. 'name' => IAccountManager::PROPERTY_DISPLAYNAME,
  202. 'value' => 'K. Cheng',
  203. 'scope' => IAccountManager::SCOPE_FEDERATED
  204. ],
  205. [
  206. 'name' => IAccountManager::PROPERTY_EMAIL,
  207. 'value' => 'cheng@emca.com',
  208. 'scope' => IAccountManager::SCOPE_FEDERATED
  209. ],
  210. [
  211. 'name' => IAccountManager::PROPERTY_TWITTER,
  212. 'value' => '', '
  213. scope' => IAccountManager::SCOPE_LOCAL
  214. ],
  215. [
  216. 'name' => IAccountManager::PROPERTY_PHONE,
  217. 'value' => '+71601212123',
  218. 'scope' => IAccountManager::SCOPE_LOCAL
  219. ],
  220. [
  221. 'name' => IAccountManager::PROPERTY_ADDRESS,
  222. 'value' => 'Pinapple Street 22',
  223. 'scope' => IAccountManager::SCOPE_LOCAL
  224. ],
  225. [
  226. 'name' => IAccountManager::PROPERTY_WEBSITE,
  227. 'value' => 'https://emca.com',
  228. 'scope' => IAccountManager::SCOPE_FEDERATED
  229. ],
  230. [
  231. 'name' => IAccountManager::COLLECTION_EMAIL,
  232. 'value' => 'k.cheng@emca.com',
  233. 'scope' => IAccountManager::SCOPE_LOCAL
  234. ],
  235. [
  236. 'name' => IAccountManager::COLLECTION_EMAIL,
  237. 'value' => 'kai.cheng@emca.com',
  238. 'scope' => IAccountManager::SCOPE_LOCAL
  239. ],
  240. ],
  241. ],
  242. [
  243. 'user' => $this->makeUser('goodpal@elpmaxe.org', 'Goodpal, Kim', 'goodpal@elpmaxe.org'),
  244. 'data' => [
  245. [
  246. 'name' => IAccountManager::PROPERTY_DISPLAYNAME,
  247. 'value' => 'Goodpal, Kim',
  248. 'scope' => IAccountManager::SCOPE_PUBLISHED
  249. ],
  250. [
  251. 'name' => IAccountManager::PROPERTY_EMAIL,
  252. 'value' => 'goodpal@elpmaxe.org',
  253. 'scope' => IAccountManager::SCOPE_PUBLISHED
  254. ],
  255. [
  256. 'name' => IAccountManager::PROPERTY_TWITTER,
  257. 'value' => '',
  258. 'scope' => IAccountManager::SCOPE_LOCAL
  259. ],
  260. [
  261. 'name' => IAccountManager::PROPERTY_PHONE,
  262. 'value' => '+71602121231',
  263. 'scope' => IAccountManager::SCOPE_FEDERATED
  264. ],
  265. [
  266. 'name' => IAccountManager::PROPERTY_ADDRESS,
  267. 'value' => 'Octopus Ave 17',
  268. 'scope' => IAccountManager::SCOPE_FEDERATED
  269. ],
  270. [
  271. 'name' => IAccountManager::PROPERTY_WEBSITE,
  272. 'value' => 'https://elpmaxe.org',
  273. 'scope' => IAccountManager::SCOPE_PUBLISHED
  274. ],
  275. ],
  276. ],
  277. ];
  278. foreach ($users as $userInfo) {
  279. $this->invokePrivate($this->accountManager, 'updateUser', [$userInfo['user'], $userInfo['data'], false]);
  280. }
  281. }
  282. /**
  283. * get a instance of the accountManager
  284. *
  285. * @param array $mockedMethods list of methods which should be mocked
  286. * @return MockObject | AccountManager
  287. */
  288. public function getInstance($mockedMethods = null) {
  289. return $this->getMockBuilder(AccountManager::class)
  290. ->setConstructorArgs([
  291. $this->connection,
  292. $this->config,
  293. $this->eventDispatcher,
  294. $this->jobList,
  295. $this->logger,
  296. ])
  297. ->setMethods($mockedMethods)
  298. ->getMock();
  299. }
  300. /**
  301. * @dataProvider dataTrueFalse
  302. *
  303. * @param array $newData
  304. * @param array $oldData
  305. * @param bool $insertNew
  306. * @param bool $updateExisting
  307. */
  308. public function testUpdateUser($newData, $oldData, $insertNew, $updateExisting) {
  309. $accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser']);
  310. /** @var IUser $user */
  311. $user = $this->createMock(IUser::class);
  312. // FIXME: should be an integration test instead of this abomination
  313. $accountManager->expects($this->once())->method('getUser')->with($user)->willReturn($oldData);
  314. if ($updateExisting) {
  315. $accountManager->expects($this->once())->method('updateExistingUser')
  316. ->with($user, $newData);
  317. $accountManager->expects($this->never())->method('insertNewUser');
  318. }
  319. if ($insertNew) {
  320. $accountManager->expects($this->once())->method('insertNewUser')
  321. ->with($user, $newData);
  322. $accountManager->expects($this->never())->method('updateExistingUser');
  323. }
  324. if (!$insertNew && !$updateExisting) {
  325. $accountManager->expects($this->never())->method('updateExistingUser');
  326. $accountManager->expects($this->never())->method('insertNewUser');
  327. $this->eventDispatcher->expects($this->never())->method('dispatch');
  328. } else {
  329. $this->eventDispatcher->expects($this->once())->method('dispatch')
  330. ->willReturnCallback(
  331. function ($eventName, $event) use ($user, $newData) {
  332. $this->assertSame('OC\AccountManager::userUpdated', $eventName);
  333. $this->assertInstanceOf(GenericEvent::class, $event);
  334. /** @var GenericEvent $event */
  335. $this->assertSame($user, $event->getSubject());
  336. $this->assertSame($newData, $event->getArguments());
  337. }
  338. );
  339. }
  340. $this->invokePrivate($accountManager, 'updateUser', [$user, $newData]);
  341. }
  342. public function dataTrueFalse() {
  343. return [
  344. #$newData | $oldData | $insertNew | $updateExisting
  345. [['myProperty' => ['value' => 'newData']], ['myProperty' => ['value' => 'oldData']], false, true],
  346. [['myProperty' => ['value' => 'oldData']], ['myProperty' => ['value' => 'oldData']], false, false]
  347. ];
  348. }
  349. public function testAddMissingDefaultValues() {
  350. $input = [
  351. ['value' => 'value1', 'verified' => '0', 'name' => 'key1'],
  352. ['value' => 'value1', 'name' => 'key2'],
  353. ];
  354. $expected = [
  355. ['value' => 'value1', 'verified' => '0', 'name' => 'key1'],
  356. ['value' => 'value1', 'name' => 'key2', 'verified' => '0'],
  357. ];
  358. $result = $this->invokePrivate($this->accountManager, 'addMissingDefaultValues', [$input]);
  359. $this->assertSame($expected, $result);
  360. }
  361. private function addDummyValuesToTable($uid, $data) {
  362. $query = $this->connection->getQueryBuilder();
  363. $query->insert($this->table)
  364. ->values(
  365. [
  366. 'uid' => $query->createNamedParameter($uid),
  367. 'data' => $query->createNamedParameter(json_encode($data)),
  368. ]
  369. )
  370. ->execute();
  371. }
  372. public function testGetAccount() {
  373. $accountManager = $this->getInstance(['getUser']);
  374. /** @var IUser $user */
  375. $user = $this->createMock(IUser::class);
  376. $data = [
  377. [
  378. 'value' => '@twitterhandle',
  379. 'scope' => IAccountManager::SCOPE_LOCAL,
  380. 'verified' => IAccountManager::NOT_VERIFIED,
  381. 'name' => IAccountManager::PROPERTY_TWITTER,
  382. ],
  383. [
  384. 'value' => 'test@example.com',
  385. 'scope' => IAccountManager::SCOPE_PUBLISHED,
  386. 'verified' => IAccountManager::VERIFICATION_IN_PROGRESS,
  387. 'name' => IAccountManager::PROPERTY_EMAIL,
  388. ],
  389. [
  390. 'value' => 'https://example.com',
  391. 'scope' => IAccountManager::SCOPE_FEDERATED,
  392. 'verified' => IAccountManager::VERIFIED,
  393. 'name' => IAccountManager::PROPERTY_WEBSITE,
  394. ],
  395. ];
  396. $expected = new Account($user);
  397. $expected->setProperty(IAccountManager::PROPERTY_TWITTER, '@twitterhandle', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
  398. $expected->setProperty(IAccountManager::PROPERTY_EMAIL, 'test@example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS);
  399. $expected->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_FEDERATED, IAccountManager::VERIFIED);
  400. $accountManager->expects($this->once())
  401. ->method('getUser')
  402. ->willReturn($data);
  403. $this->assertEquals($expected, $accountManager->getAccount($user));
  404. }
  405. public function dataParsePhoneNumber(): array {
  406. return [
  407. ['0711 / 25 24 28-90', 'DE', '+4971125242890'],
  408. ['0711 / 25 24 28-90', '', null],
  409. ['+49 711 / 25 24 28-90', '', '+4971125242890'],
  410. ];
  411. }
  412. /**
  413. * @dataProvider dataParsePhoneNumber
  414. * @param string $phoneInput
  415. * @param string $defaultRegion
  416. * @param string|null $phoneNumber
  417. */
  418. public function testParsePhoneNumber(string $phoneInput, string $defaultRegion, ?string $phoneNumber): void {
  419. $this->config->method('getSystemValueString')
  420. ->willReturn($defaultRegion);
  421. if ($phoneNumber === null) {
  422. $this->expectException(\InvalidArgumentException::class);
  423. self::invokePrivate($this->accountManager, 'parsePhoneNumber', [$phoneInput]);
  424. } else {
  425. self::assertEquals($phoneNumber, self::invokePrivate($this->accountManager, 'parsePhoneNumber', [$phoneInput]));
  426. }
  427. }
  428. public function dataParseWebsite(): array {
  429. return [
  430. ['https://nextcloud.com', 'https://nextcloud.com'],
  431. ['http://nextcloud.com', 'http://nextcloud.com'],
  432. ['ftp://nextcloud.com', null],
  433. ['//nextcloud.com/', null],
  434. ['https:///?query', null],
  435. ];
  436. }
  437. /**
  438. * @dataProvider dataParseWebsite
  439. * @param string $websiteInput
  440. * @param string|null $websiteOutput
  441. */
  442. public function testParseWebsite(string $websiteInput, ?string $websiteOutput): void {
  443. if ($websiteOutput === null) {
  444. $this->expectException(\InvalidArgumentException::class);
  445. self::invokePrivate($this->accountManager, 'parseWebsite', [$websiteInput]);
  446. } else {
  447. self::assertEquals($websiteOutput, self::invokePrivate($this->accountManager, 'parseWebsite', [$websiteInput]));
  448. }
  449. }
  450. /**
  451. * @dataProvider searchDataProvider
  452. */
  453. public function testSearchUsers(string $property, array $values, array $expected): void {
  454. $this->populateOrUpdate();
  455. $matchedUsers = $this->accountManager->searchUsers($property, $values);
  456. foreach ($expected as $expectedEntry) {
  457. $this->assertContains($expectedEntry, $matchedUsers);
  458. }
  459. if (empty($expected)) {
  460. $this->assertEmpty($matchedUsers);
  461. }
  462. }
  463. public function searchDataProvider(): array {
  464. return [
  465. [ #0 Search for an existing name
  466. IAccountManager::PROPERTY_DISPLAYNAME,
  467. ['Jane Doe'],
  468. ['Jane Doe' => 'j.doe']
  469. ],
  470. [ #1 Search for part of a name (no result)
  471. IAccountManager::PROPERTY_DISPLAYNAME,
  472. ['Jane'],
  473. []
  474. ],
  475. [ #2 Search for part of a name (no result, test wildcard)
  476. IAccountManager::PROPERTY_DISPLAYNAME,
  477. ['Jane%'],
  478. []
  479. ],
  480. [ #3 Search for phone
  481. IAccountManager::PROPERTY_PHONE,
  482. ['+491603121212'],
  483. ['+491603121212' => 'b32c5a5b-1084-4380-8856-e5223b16de9f'],
  484. ],
  485. [ #4 Search for twitter handles
  486. IAccountManager::PROPERTY_TWITTER,
  487. ['@sometwitter', '@a_alice', '@unseen'],
  488. ['@sometwitter' => 'j.doe', '@a_alice' => 'a.allison'],
  489. ],
  490. [ #5 Search for email
  491. IAccountManager::PROPERTY_EMAIL,
  492. ['cheng@emca.com'],
  493. ['cheng@emca.com' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'],
  494. ],
  495. [ #6 Search for email by additional email
  496. IAccountManager::PROPERTY_EMAIL,
  497. ['kai.cheng@emca.com'],
  498. ['kai.cheng@emca.com' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'],
  499. ],
  500. [ #7 Search for additional email
  501. IAccountManager::COLLECTION_EMAIL,
  502. ['kai.cheng@emca.com', 'cheng@emca.com'],
  503. ['kai.cheng@emca.com' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'],
  504. ],
  505. [ #8 Search for email by additional email (two valid search values, but the same user)
  506. IAccountManager::PROPERTY_EMAIL,
  507. ['kai.cheng@emca.com', 'cheng@emca.com'],
  508. [
  509. 'kai.cheng@emca.com' => '31b5316a-9b57-4b17-970a-315a4cbe73eb',
  510. ],
  511. ],
  512. ];
  513. }
  514. }