aboutsummaryrefslogtreecommitdiffstats
path: root/tests/lib/Comments/ManagerTest.php
blob: 671389232e2c661f6c0698e05f6fd4976d5afd2f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
<?php

/**
 * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OC\Encryption;

use InvalidArgumentException;
use OC\Files\Filesystem;
use OC\Files\Mount;
use OC\Files\View;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use Psr\Log\LoggerInterface;

/**
 * update encrypted files, e.g. because a file was shared
 */
class Update {
	/** @var View */
	protected $view;

	/** @var Util */
	protected $util;

	/** @var \OC\Files\Mount\Manager */
	protected $mountManager;

	/** @var Manager */
	protected $encryptionManager;

	/** @var string */
	protected $uid;

	/** @var File */
	protected $file;

	/** @var LoggerInterface */
	protected $logger;

	/**
	 * @param string $uid
	 */
	public function __construct(
		View $view,
		Util $util,
		Mount\Manager $mountManager,
		Manager $encryptionManager,
		File $file,
		LoggerInterface $logger,
		$uid,
	) {
		$this->view = $view;
		$this->util = $util;
		$this->mountManager = $mountManager;
		$this->encryptionManager = $encryptionManager;
		$this->file = $file;
		$this->logger = $logger;
		$this->uid = $uid;
	}

	/**
	 * hook after file was shared
	 *
	 * @param array $params
	 */
	public function postShared($params) {
		if ($this->encryptionManager->isEnabled()) {
			if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') {
				$path = Filesystem::getPath($params['fileSource']);
				[$owner, $ownerPath] = $this->getOwnerPath($path);
				$absPath = '/' . $owner . '/files/' . $ownerPath;
				$this->update($absPath);
			}
		}
	}

	/**
	 * hook after file was unshared
	 *
	 * @param array $params
	 */
	public function postUnshared($params) {
		if ($this->encryptionManager->isEnabled()) {
			if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') {
				$path = Filesystem::getPath($params['fileSource']);
				[$owner, $ownerPath] = $this->getOwnerPath($path);
				$absPath = '/' . $owner . '/files/' . $ownerPath;
				$this->update($absPath);
			}
		}
	}

	/**
	 * inform encryption module that a file was restored from the trash bin,
	 * e.g. to update the encryption keys
	 *
	 * @param array $params
	 */
	public function postRestore($params) {
		if ($this->encryptionManager->isEnabled()) {
			$path = Filesystem::normalizePath('/' . $this->uid . '/files/' . $params['filePath']);
			$this->update($path);
		}
	}

	/**
	 * inform encryption module that a file was renamed,
	 * e.g. to update the encryption keys
	 *
	 * @param array $params
	 */
	public function postRename($params) {
		$source = $params['oldpath'];
		$target = $params['newpath'];
		if (
			$this->encryptionManager->isEnabled() &&
			dirname($source) !== dirname($target)
		) {
			[$owner, $ownerPath] = $this->getOwnerPath($target);
			$absPath = '/' . $owner . '/files/' . $ownerPath;
			$this->update($absPath);
		}
	}

	/**
	 * get owner and path relative to data/<owner>/files
	 *
	 * @param string $path path to file for current user
	 * @return array ['owner' => $owner, 'path' => $path]
	 * @throws \InvalidArgumentException
	 */
	protected function getOwnerPath($path) {
		$info = Filesystem::getFileInfo($path);
		$owner = Filesystem::getOwner($path);
		$view = new View('/' . $owner . '/files');
		$path = $view->getPath($info->getId());
		if ($path === null) {
			throw new InvalidArgumentException('No file found for ' . $info->getId());
		}

		return [$owner, $path];
	}

	/**
	 * notify encryption module about added/removed users from a file/folder
	 *
	 * @param string $path relative to data/
	 * @throws Exceptions\ModuleDoesNotExistsException
	 */
	public function update($path) {
		$encryptionModule = $this->encryptionManager->getEncryptionModule();

		// if the encryption module doesn't encrypt the files on a per-user basis
		// we have nothing to do here.
		if ($encryptionModule->needDetailedAccessList() === falsepre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
<?php

namespace Test\Comments;

use OC\Comments\Comment;
use OC\Comments\ManagerFactory;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsEventHandler;
use OCP\Comments\ICommentsManager;
use OCP\Comments\NotFoundException;
use OCP\IDBConnection;
use OCP\IUser;
use Test\TestCase;

/**
 * Class ManagerTest
 *
 * @group DB
 */
class ManagerTest extends TestCase {
	/** @var IDBConnection */
	private $connection;

	public function setUp() {
		parent::setUp();

		$this->connection = \OC::$server->getDatabaseConnection();

		$sql = $this->connection->getDatabasePlatform()->getTruncateTableSQL('`*PREFIX*comments`');
		$this->connection->prepare($sql)->execute();
	}

	protected function addDatabaseEntry($parentId, $topmostParentId, $creationDT = null, $latestChildDT = null, $objectId = null) {
		if (is_null($creationDT)) {
			$creationDT = new \DateTime();
		}
		if (is_null($latestChildDT)) {
			$latestChildDT = new \DateTime('yesterday');
		}
		if (is_null($objectId)) {
			$objectId = 'file64';
		}

		$qb = $this->connection->getQueryBuilder();
		$qb
			->insert('comments')
			->values([
				'parent_id' => $qb->createNamedParameter($parentId),
				'topmost_parent_id' => $qb->createNamedParameter($topmostParentId),
				'children_count' => $qb->createNamedParameter(2),
				'actor_type' => $qb->createNamedParameter('users'),
				'actor_id' => $qb->createNamedParameter('alice'),
				'message' => $qb->createNamedParameter('nice one'),
				'verb' => $qb->createNamedParameter('comment'),
				'creation_timestamp' => $qb->createNamedParameter($creationDT, 'datetime'),
				'latest_child_timestamp' => $qb->createNamedParameter($latestChildDT, 'datetime'),
				'object_type' => $qb->createNamedParameter('files'),
				'object_id' => $qb->createNamedParameter($objectId),
			])
			->execute();

		return $qb->getLastInsertId();
	}

	protected function getManager() {
		$factory = new ManagerFactory(\OC::$server);
		return $factory->getManager();
	}

	/**
	 * @expectedException \OCP\Comments\NotFoundException
	 */
	public function testGetCommentNotFound() {
		$manager = $this->getManager();
		$manager->get('22');
	}

	/**
	 * @expectedException \InvalidArgumentException
	 */
	public function testGetCommentNotFoundInvalidInput() {
		$manager = $this->getManager();
		$manager->get('unexisting22');
	}

	public function testGetComment() {
		$manager = $this->getManager();

		$creationDT = new \DateTime();
		$latestChildDT = new \DateTime('yesterday');

		$qb = \OC::$server->getDatabaseConnection()->getQueryBuilder();
		$qb
			->insert('comments')
			->values([
				'parent_id' => $qb->createNamedParameter('2'),
				'topmost_parent_id' => $qb->createNamedParameter('1'),
				'children_count' => $qb->createNamedParameter(2),
				'actor_type' => $qb->createNamedParameter('users'),
				'actor_id' => $qb->createNamedParameter('alice'),
				'message' => $qb->createNamedParameter('nice one'),
				'verb' => $qb->createNamedParameter('comment'),
				'creation_timestamp' => $qb->createNamedParameter($creationDT, 'datetime'),
				'latest_child_timestamp' => $qb->createNamedParameter($latestChildDT, 'datetime'),
				'object_type' => $qb->createNamedParameter('files'),
				'object_id' => $qb->createNamedParameter('file64'),
			])
			->execute();

		$id = strval($qb->getLastInsertId());

		$comment = $manager->get($id);
		$this->assertTrue($comment instanceof IComment);
		$this->assertSame($comment->getId(), $id);
		$this->assertSame($comment->getParentId(), '2');
		$this->assertSame($comment->getTopmostParentId(), '1');
		$this->assertSame($comment->getChildrenCount(), 2);
		$this->assertSame($comment->getActorType(), 'users');
		$this->assertSame($comment->getActorId(), 'alice');
		$this->assertSame($comment->getMessage(), 'nice one');
		$this->assertSame($comment->getVerb(), 'comment');
		$this->assertSame($comment->getObjectType(), 'files');
		$this->assertSame($comment->getObjectId(), 'file64');
		$this->assertEquals($comment->getCreationDateTime()->getTimestamp(), $creationDT->getTimestamp());
		$this->assertEquals($comment->getLatestChildDateTime(), $latestChildDT);
	}

	/**
	 * @expectedException \OCP\Comments\NotFoundException
	 */
	public function testGetTreeNotFound() {
		$manager = $this->getManager();
		$manager->getTree('22');
	}

	/**
	 * @expectedException \InvalidArgumentException
	 */
	public function testGetTreeNotFoundInvalidIpnut() {
		$manager = $this->getManager();
		$manager->getTree('unexisting22');
	}

	public function testGetTree() {
		$headId = $this->addDatabaseEntry(0, 0);

		$this->addDatabaseEntry($headId, $headId, new \DateTime('-3 hours'));
		$this->addDatabaseEntry($headId, $headId, new \DateTime('-2 hours'));
		$id = $this->addDatabaseEntry($headId, $headId, new \DateTime('-1 hour'));

		$manager = $this->getManager();
		$tree = $manager->getTree($headId);

		// Verifying the root comment
		$this->assertTrue(isset($tree['comment']));
		$this->assertTrue($tree['comment'] instanceof IComment);
		$this->assertSame($tree['comment']->getId(), strval($headId));
		$this->assertTrue(isset($tree['replies']));
		$this->assertSame(count($tree['replies']), 3);

		// one level deep
		foreach ($tree['replies'] as $reply) {
			$this->assertTrue($reply['comment'] instanceof IComment);
			$this->assertSame($reply['comment']->getId(), strval($id));
			$this->assertSame(count($reply['replies']), 0);
			$id--;
		}
	}

	public function testGetTreeNoReplies() {
		$id = $this->addDatabaseEntry(0, 0);

		$manager = $this->getManager();
		$tree = $manager->getTree($id);

		// Verifying the root comment
		$this->assertTrue(isset($tree['comment']));
		$this->assertTrue($tree['comment'] instanceof IComment);
		$this->assertSame($tree['comment']->getId(), strval($id));
		$this->assertTrue(isset($tree['replies']));
		$this->assertSame(count($tree['replies']), 0);

		// one level deep
		foreach ($tree['replies'] as $reply) {
			throw new \Exception('This ain`t happen');
		}
	}

	public function testGetTreeWithLimitAndOffset() {
		$headId = $this->addDatabaseEntry(0, 0);

		$this->addDatabaseEntry($headId, $headId, new \DateTime('-3 hours'));
		$this->addDatabaseEntry($headId, $headId, new \DateTime('-2 hours'));
		$this->addDatabaseEntry($headId, $headId, new \DateTime('-1 hour'));
		$idToVerify = $this->addDatabaseEntry($headId, $headId, new \DateTime());

		$manager = $this->getManager();

		for ($offset = 0; $offset < 3; $offset += 2) {
			$tree = $manager->getTree(strval($headId), 2, $offset);

			// Verifying the root comment
			$this->assertTrue(isset($tree['comment']));
			$this->assertTrue($tree['comment'] instanceof IComment);
			$this->assertSame($tree['comment']->getId(), strval($headId));
			$this->assertTrue(isset($tree['replies']));
			$this->assertSame(count($tree['replies']), 2);

			// one level deep
			foreach ($tree['replies'] as $reply) {
				$this->assertTrue($reply['comment'] instanceof IComment);
				$this->assertSame($reply['comment']->getId(), strval($idToVerify));
				$this->assertSame(count($reply['replies']), 0);
				$idToVerify--;
			}
		}
	}

	public function testGetForObject() {
		$this->addDatabaseEntry(0, 0);

		$manager = $this->getManager();
		$comments = $manager->getForObject('files', 'file64');

		$this->assertTrue(is_array($comments));
		$this->assertSame(count($comments), 1);
		$this->assertTrue($comments[0] instanceof IComment);
		$this->assertSame($comments[0]->getMessage(), 'nice one');
	}

	public function testGetForObjectWithLimitAndOffset() {
		$this->addDatabaseEntry(0, 0, new \DateTime('-6 hours'));
		$this->addDatabaseEntry(0, 0, new \DateTime('-5 hours'));
		$this->addDatabaseEntry(1, 1, new \DateTime('-4 hours'));
		$this->addDatabaseEntry(0, 0, new \DateTime('-3 hours'));
		$this->addDatabaseEntry(2, 2, new \DateTime('-2 hours'));
		$this->addDatabaseEntry(2, 2, new \DateTime('-1 hours'));
		$idToVerify = $this->addDatabaseEntry(3, 1, new \DateTime());

		$manager = $this->getManager();
		$offset = 0;
		do {
			$comments = $manager->getForObject('files', 'file64', 3, $offset);

			$this->assertTrue(is_array($comments));
			foreach ($comments as $comment) {
				$this->assertTrue($comment instanceof IComment);
				$this->assertSame($comment->getMessage(), 'nice one');
				$this->assertSame($comment->getId(), strval($idToVerify));
				$idToVerify--;
			}
			$offset += 3;
		} while (count($comments) > 0);
	}

	public function testGetForObjectWithDateTimeConstraint() {
		$this->addDatabaseEntry(0, 0, new \DateTime('-6 hours'));
		$this->addDatabaseEntry(0, 0, new \DateTime('-5 hours'));
		$id1 = $this->addDatabaseEntry(0, 0, new \DateTime('-3 hours'));
		$id2 = $this->addDatabaseEntry(2, 2, new \DateTime('-2 hours'));

		$manager = $this->getManager();
		$comments = $manager->getForObject('files', 'file64', 0, 0, new \DateTime('-4 hours'));

		$this->assertSame(count($comments), 2);
		$this->assertSame($comments[0]->getId(), strval($id2));
		$this->assertSame($comments[1]->getId(), strval($id1));
	}

	public function testGetForObjectWithLimitAndOffsetAndDateTimeConstraint() {
		$this->addDatabaseEntry(0, 0, new \DateTime('-7 hours'));
		$this->addDatabaseEntry(0, 0, new \DateTime('-6 hours'));
		$this->addDatabaseEntry(1, 1, new \DateTime('-5 hours'));
		$this->addDatabaseEntry(0, 0, new \DateTime('-3 hours'));
		$this->addDatabaseEntry(2, 2, new \DateTime('-2 hours'));
		$this->addDatabaseEntry(2, 2, new \DateTime('-1 hours'));
		$idToVerify = $this->addDatabaseEntry(3, 1, new \DateTime());

		$manager = $this->getManager();
		$offset = 0;
		do {
			$comments = $manager->getForObject('files', 'file64', 3, $offset, new \DateTime('-4 hours'));

			$this->assertTrue(is_array($comments));
			foreach ($comments as $comment) {
				$this->assertTrue($comment instanceof IComment);
				$this->assertSame($comment->getMessage(), 'nice one');
				$this->assertSame($comment->getId(), strval($idToVerify));
				$this->assertTrue(intval($comment->getId()) >= 4);
				$idToVerify--;
			}
			$offset += 3;
		} while (count($comments) > 0);
	}

	public function testGetNumberOfCommentsForObject() {
		for ($i = 1; $i < 5; $i++) {
			$this->addDatabaseEntry(0, 0);
		}

		$manager = $this->getManager();

		$amount = $manager->getNumberOfCommentsForObject('untype', '00');
		$this->assertSame($amount, 0);

		$amount = $manager->getNumberOfCommentsForObject('files', 'file64');
		$this->assertSame($amount, 4);
	}

	public function testGetNumberOfUnreadCommentsForFolder() {
		$query = $this->connection->getQueryBuilder();
		$query->insert('filecache')
			->values([
				'parent' => $query->createNamedParameter(1000),
				'size' => $query->createNamedParameter(10),
				'mtime' => $query->createNamedParameter(10),
				'storage_mtime' => $query->createNamedParameter(10),
				'path' => $query->createParameter('path'),
				'path_hash' => $query->createParameter('path'),
			]);

		$fileIds = [];
		for ($i = 0; $i < 4; $i++) {
			$query->setParameter('path', 'path_' . $i);
			$query->execute();
			$fileIds[] = $query->getLastInsertId();
		}

		// 2 comment for 1111 with 1 before read marker
		// 2 comments for 1112 with no read marker
		// 1 comment for 1113 before read marker
		// 1 comment for 1114 with no read marker
		$this->addDatabaseEntry(0, 0, null, null, $fileIds[1]);
		for ($i = 0; $i < 4; $i++) {
			$this->addDatabaseEntry(0, 0, null, null, $fileIds[$i]);
		}
		$this->addDatabaseEntry(0, 0, (new \DateTime())->modify('-2 days'), null, $fileIds[0]);
		/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
		$user = $this->createMock(IUser::class);
		$user->expects($this->any())
			->method('getUID')
			->will($this->returnValue('comment_test'));

		$manager = $this->getManager();

		$manager->setReadMark('files', (string) $fileIds[0], (new \DateTime())->modify('-1 days'), $user);
		$manager->setReadMark('files', (string) $fileIds[2], (new \DateTime()), $user);

		$amount = $manager->getNumberOfUnreadCommentsForFolder(1000, $user);
		$this->assertEquals([
			$fileIds[0] => 1,
			$fileIds[1] => 2,
			$fileIds[3] => 1,
		], $amount);
	}

	public function invalidCreateArgsProvider() {
		return [
			['', 'aId-1', 'oType-1', 'oId-1'],
			['aType-1', '', 'oType-1', 'oId-1'],
			['aType-1', 'aId-1', '', 'oId-1'],
			['aType-1', 'aId-1', 'oType-1', ''],
			[1, 'aId-1', 'oType-1', 'oId-1'],
			['aType-1', 1, 'oType-1', 'oId-1'],
			['aType-1', 'aId-1', 1, 'oId-1'],
			['aType-1', 'aId-1', 'oType-1', 1],
		];
	}

	/**
	 * @dataProvider invalidCreateArgsProvider
	 * @expectedException \InvalidArgumentException
	 * @param string $aType
	 * @param string $aId
	 * @param string $oType
	 * @param string $oId
	 */
	public function testCreateCommentInvalidArguments($aType, $aId, $oType, $oId) {
		$manager = $this->getManager();
		$manager->create($aType, $aId, $oType, $oId);
	}

	public function testCreateComment() {
		$actorType = 'bot';
		$actorId = 'bob';
		$objectType = 'weather';
		$objectId = 'bielefeld';

		$comment = $this->getManager()->create($actorType, $actorId, $objectType, $objectId);
		$this->assertTrue($comment instanceof IComment);
		$this->assertSame($comment->getActorType(), $actorType);
		$this->assertSame($comment->getActorId(), $actorId);
		$this->assertSame($comment->getObjectType(), $objectType);
		$this->assertSame($comment->getObjectId(), $objectId);
	}

	/**
	 * @expectedException \OCP\Comments\NotFoundException
	 */
	public function testDelete() {
		$manager = $this->getManager();

		$done = $manager->delete('404');
		$this->assertFalse($done);

		$done = $manager->delete('%');
		$this->assertFalse($done);

		$done = $manager->delete('');
		$this->assertFalse($done);

		$id = strval($this->addDatabaseEntry(0, 0));
		$comment = $manager->get($id);
		$this->assertTrue($comment instanceof IComment);
		$done = $manager->delete($id);
		$this->assertTrue($done);
		$manager->get($id);
	}

	public function testSaveNew() {
		$manager = $this->getManager();
		$comment = new Comment();
		$comment
			->setActor('users', 'alice')
			->setObject('files', 'file64')
			->setMessage('very beautiful, I am impressed!')
			->setVerb('comment');

		$saveSuccessful = $manager->save($comment);
		$this->assertTrue($saveSuccessful);
		$this->assertTrue($comment->getId() !== '');
		$this->assertTrue($comment->getId() !== '0');
		$this->assertTrue(!is_null($comment->getCreationDateTime()));

		$loadedComment = $manager->get($comment->getId());
		$this->assertSame($comment->getMessage(), $loadedComment->getMessage());
		$this->assertEquals($comment->getCreationDateTime()->getTimestamp(), $loadedComment->getCreationDateTime()->getTimestamp());
	}

	public function testSaveUpdate() {
		$manager = $this->getManager();
		$comment = new Comment();
		$comment
			->setActor('users', 'alice')
			->setObject('files', 'file64')
			->setMessage('very beautiful, I am impressed!')
			->setVerb('comment');

		$manager->save($comment);

		$comment->setMessage('very beautiful, I am really so much impressed!');
		$manager->save($comment);

		$loadedComment = $manager->get($comment->getId());
		$this->assertSame($comment->getMessage(), $loadedComment->getMessage());
	}

	/**
	 * @expectedException \OCP\Comments\NotFoundException
	 */
	public function testSaveUpdateException() {
		$manager = $this->getManager();
		$comment = new Comment();
		$comment
			->setActor('users', 'alice')
			->setObject('files', 'file64')
			->setMessage('very beautiful, I am impressed!')
			->setVerb('comment');

		$manager->save($comment);

		$manager->delete($comment->getId());
		$comment->setMessage('very beautiful, I am really so much impressed!');
		$manager->save($comment);
	}

	/**
	 * @expectedException \UnexpectedValueException
	 */
	public function testSaveIncomplete() {
		$manager = $this->getManager();
		$comment = new Comment();
		$comment->setMessage('from no one to nothing');
		$manager->save($comment);
	}

	public function testSaveAsChild() {
		$id = $this->addDatabaseEntry(0, 0);

		$manager = $this->getManager();

		for ($i = 0; $i < 3; $i++) {
			$comment = new Comment();
			$comment
				->setActor('users', 'alice')
				->setObject('files', 'file64')
				->setParentId(strval($id))
				->setMessage('full ack')
				->setVerb('comment')
				// setting the creation time avoids using sleep() while making sure to test with different timestamps
				->setCreationDateTime(new \DateTime('+' . $i . ' minutes'));

			$manager->save($comment);

			$this->assertSame($comment->getTopmostParentId(), strval($id));
			$parentComment = $manager->get(strval($id));
			$this->assertSame($parentComment->getChildrenCount(), $i + 1);
			$this->assertEquals($parentComment->getLatestChildDateTime()->getTimestamp(), $comment->getCreationDateTime()->getTimestamp());
		}
	}

	public function invalidActorArgsProvider() {
		return
			[
				['', ''],
				[1, 'alice'],
				['users', 1],
			];
	}

	/**
	 * @dataProvider invalidActorArgsProvider
	 * @expectedException \InvalidArgumentException
	 * @param string $type
	 * @param string $id
	 */
	public function testDeleteReferencesOfActorInvalidInput($type, $id) {
		$manager = $this->getManager();
		$manager->deleteReferencesOfActor($type, $id);
	}

	public function testDeleteReferencesOfActor() {
		$ids = [];
		$ids[] = $this->addDatabaseEntry(0, 0);
		$ids[] = $this->addDatabaseEntry(0, 0);
		$ids[] = $this->addDatabaseEntry(0, 0);

		$manager = $this->getManager();

		// just to make sure they are really set, with correct actor data
		$comment = $manager->get(strval($ids[1]));
		$this->assertSame($comment->getActorType(), 'users');
		$this->assertSame($comment->getActorId(), 'alice');

		$wasSuccessful = $manager->deleteReferencesOfActor('users', 'alice');
		$this->assertTrue($wasSuccessful);

		foreach ($ids as $id) {
			$comment = $manager->get(strval($id));
			$this->assertSame($comment->getActorType(), ICommentsManager::DELETED_USER);
			$this->assertSame($comment->getActorId(), ICommentsManager::DELETED_USER);
		}

		// actor info is gone from DB, but when database interaction is alright,
		// we still expect to get true back
		$wasSuccessful = $manager->deleteReferencesOfActor('users', 'alice');
		$this->assertTrue($wasSuccessful);
	}

	public function testDeleteReferencesOfActorWithUserManagement() {
		$user = \OC::$server->getUserManager()->createUser('xenia', '123456');
		$this->assertTrue($user instanceof IUser);

		$manager = \OC::$server->getCommentsManager();
		$comment = $manager->create('users', $user->getUID(), 'files', 'file64');
		$comment
			->setMessage('Most important comment I ever left on the Internet.')
			->setVerb('comment');
		$status = $manager->save($comment);
		$this->assertTrue($status);

		$commentID = $comment->getId();
		$user->delete();

		$comment = $manager->get($commentID);
		$this->assertSame($comment->getActorType(), ICommentsManager::DELETED_USER);
		$this->assertSame($comment->getActorId(), ICommentsManager::DELETED_USER);
	}

	public function invalidObjectArgsProvider() {
		return
			[
				['', ''],
				[1, 'file64'],
				['files', 1],
			];
	}

	/**
	 * @dataProvider invalidObjectArgsProvider
	 * @expectedException \InvalidArgumentException
	 * @param string $type
	 * @param string $id
	 */
	public function testDeleteCommentsAtObjectInvalidInput($type, $id) {
		$manager = $this->getManager();
		$manager->deleteCommentsAtObject($type, $id);
	}

	public function testDeleteCommentsAtObject() {
		$ids = [];
		$ids[] = $this->addDatabaseEntry(0, 0);
		$ids[] = $this->addDatabaseEntry(0, 0);
		$ids[] = $this->addDatabaseEntry(0, 0);

		$manager = $this->getManager();

		// just to make sure they are really set, with correct actor data
		$comment = $manager->get(strval($ids[1]));
		$this->assertSame($comment->getObjectType(), 'files');
		$this->assertSame($comment->getObjectId(), 'file64');

		$wasSuccessful = $manager->deleteCommentsAtObject('files', 'file64');
		$this->assertTrue($wasSuccessful);

		$verified = 0;
		foreach ($ids as $id) {
			try {
				$manager->get(strval($id));
			} catch (NotFoundException $e) {
				$verified++;
			}
		}
		$this->assertSame($verified, 3);

		// actor info is gone from DB, but when database interaction is alright,
		// we still expect to get true back
		$wasSuccessful = $manager->deleteCommentsAtObject('files', 'file64');
		$this->assertTrue($wasSuccessful);
	}

	public function testSetMarkRead() {
		/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
		$user = $this->createMock(IUser::class);
		$user->expects($this->any())
			->method('getUID')
			->will($this->returnValue('alice'));

		$dateTimeSet = new \DateTime();

		$manager = $this->getManager();
		$manager->setReadMark('robot', '36', $dateTimeSet, $user);

		$dateTimeGet = $manager->getReadMark('robot', '36', $user);

		$this->assertEquals($dateTimeGet->getTimestamp(), $dateTimeSet->getTimestamp());
	}

	public function testSetMarkReadUpdate() {
		/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
		$user = $this->createMock(IUser::class);
		$user->expects($this->any())
			->method('getUID')
			->will($this->returnValue('alice'));

		$dateTimeSet = new \DateTime('yesterday');

		$manager = $this->getManager();
		$manager->setReadMark('robot', '36', $dateTimeSet, $user);

		$dateTimeSet = new \DateTime('today');
		$manager->setReadMark('robot', '36', $dateTimeSet, $user);

		$dateTimeGet = $manager->getReadMark('robot', '36', $user);

		$this->assertEquals($dateTimeGet, $dateTimeSet);
	}

	public function testReadMarkDeleteUser() {
		/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
		$user = $this->createMock(IUser::class);
		$user->expects($this->any())
			->method('getUID')
			->will($this->returnValue('alice'));

		$dateTimeSet = new \DateTime();

		$manager = $this->getManager();
		$manager->setReadMark('robot', '36', $dateTimeSet, $user);

		$manager->deleteReadMarksFromUser($user);
		$dateTimeGet = $manager->getReadMark('robot', '36', $user);

		$this->assertNull($dateTimeGet);
	}

	public function testReadMarkDeleteObject() {
		/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
		$user = $this->createMock(IUser::class);
		$user->expects($this->any())
			->method('getUID')
			->will($this->returnValue('alice'));

		$dateTimeSet = new \DateTime();

		$manager = $this->getManager();
		$manager->setReadMark('robot', '36', $dateTimeSet, $user);

		$manager->deleteReadMarksOnObject('robot', '36');
		$dateTimeGet = $manager->getReadMark('robot', '36', $user);

		$this->assertNull($dateTimeGet);
	}

	public function testSendEvent() {
		$handler1 = $this->getMockBuilder(ICommentsEventHandler::class)->getMock();
		$handler1->expects($this->exactly(4))
			->method('handle');

		$handler2 = $this->getMockBuilder(ICommentsEventHandler::class)->getMock();
		$handler1->expects($this->exactly(4))
			->method('handle');

		$manager = $this->getManager();
		$manager->registerEventHandler(function () use ($handler1) {
			return $handler1;
		});
		$manager->registerEventHandler(function () use ($handler2) {
			return $handler2;
		});

		$comment = new Comment();
		$comment
			->setActor('users', 'alice')
			->setObject('files', 'file64')
			->setMessage('very beautiful, I am impressed!')
			->setVerb('comment');

		// Add event
		$manager->save($comment);

		// Update event
		$comment->setMessage('Different topic');
		$manager->save($comment);

		// Delete event
		$manager->delete($comment->getId());
	}

	public function testResolveDisplayName() {
		$manager = $this->getManager();

		$planetClosure = function ($name) {
			return ucfirst($name);
		};

		$galaxyClosure = function ($name) {
			return strtoupper($name);
		};

		$manager->registerDisplayNameResolver('planet', $planetClosure);
		$manager->registerDisplayNameResolver('galaxy', $galaxyClosure);

		$this->assertSame('Neptune', $manager->resolveDisplayName('planet', 'neptune'));
		$this->assertSame('SOMBRERO', $manager->resolveDisplayName('galaxy', 'sombrero'));
	}

	/**
	 * @expectedException \OutOfBoundsException
	 */
	public function testRegisterResolverDuplicate() {
		$manager = $this->getManager();

		$planetClosure = function ($name) {
			return ucfirst($name);
		};
		$manager->registerDisplayNameResolver('planet', $planetClosure);
		$manager->registerDisplayNameResolver('planet', $planetClosure);
	}

	/**
	 * @expectedException \InvalidArgumentException
	 */
	public function testRegisterResolverInvalidType() {
		$manager = $this->getManager();

		$planetClosure = function ($name) {
			return ucfirst($name);
		};
		$manager->registerDisplayNameResolver(1337, $planetClosure);
	}

	/**
	 * @expectedException \OutOfBoundsException
	 */
	public function testResolveDisplayNameUnregisteredType() {
		$manager = $this->getManager();

		$planetClosure = function ($name) {
			return ucfirst($name);
		};

		$manager->registerDisplayNameResolver('planet', $planetClosure);
		$manager->resolveDisplayName('galaxy', 'sombrero');
	}

	public function testResolveDisplayNameDirtyResolver() {
		$manager = $this->getManager();

		$planetClosure = function () {
			return null;
		};

		$manager->registerDisplayNameResolver('planet', $planetClosure);
		$this->assertTrue(is_string($manager->resolveDisplayName('planet', 'neptune')));
	}

	/**
	 * @expectedException \InvalidArgumentException
	 */
	public function testResolveDisplayNameInvalidType() {
		$manager = $this->getManager();

		$planetClosure = function () {
			return null;
		};

		$manager->registerDisplayNameResolver('planet', $planetClosure);
		$this->assertTrue(is_string($manager->resolveDisplayName(1337, 'neptune')));
	}

}