| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Test; |
| 39 | |
| 40 | require_once __DIR__.'/../../../autoload.php'; |
| 41 | |
| 42 | use Std\Lib\Tester; |
| 43 | use Std\Lib\Activitypub; |
| 44 | use Std\Lib\Comments; |
| 45 | use Std\Lib\Notifications; |
| 46 | use Std\Lib\Fedi\{Fedi, Notes, Remote, Social}; |
| 47 | |
| 48 | $test = new Tester([ |
| 49 | 'colorOutput' => true, |
| 50 | 'verboseOutput' => true |
| 51 | ]); |
| 52 | |
| 53 | // 本物のデータを触らない様に、一時ディレクトリを使う |
| 54 | $tmp = sys_get_temp_dir().'/lbcomments_'.bin2hex(random_bytes(6)); |
| 55 | Comments::$dataDir = Notifications::$dataDir = "{$tmp}/data"; |
| 56 | Fedi::$dataDir = "{$tmp}/ap"; |
| 57 | Fedi::$userDir = "{$tmp}/user/"; |
| 58 | mkdir(Fedi::$userDir, 0700, true); |
| 59 | $mkuser = function(string $name, int $id, int $role = \Roles::MEMBER) use ($tmp): \stdClass { |
| 60 | $u = ['id' => $id, 'username' => $name, 'displayname' => '', 'namecolor' => '', 'avatar' => '', 'gender' => -1, 'role' => $role]; |
| 61 | file_put_contents("{$tmp}/user/{$id}.{$name}.json", json_encode($u)); |
| 62 | return (object)$u; |
| 63 | }; |
| 64 | $admin = $mkuser('admin1234', 1, \Roles::MEMBER | \Roles::ADMIN); |
| 65 | $alice = $mkuser('alice1234', 2); |
| 66 | $bob = $mkuser('bob12345', 3); |
| 67 | $banned = $mkuser('banned123', 4, \Roles::BANNED); |
| 68 | $types = fn(string $u) => array_map(fn($n) => $n['type'].':'.($n['from']['username'] ?? $n['from']['actor'] ?? ''), Notifications::list($u)); |
| 69 | $thread = 'blog/test-post'; |
| 70 | |
| 71 | $test->describe('コメント', function($test) use ($admin, $alice, $bob, $banned, $types, $thread): void { |
| 72 | if (!Comments::enabled()) $test->skipAll('COMMENTS_ENABLEDが無効'); |
| 73 | |
| 74 | $test->it('不正な入力ではコメント出来ないはず', function($test) use ($alice, $banned, $thread): void { |
| 75 | $test->assertFalse(Comments::addLocal($thread, '記事', $alice, " \n ")->isSuccess, '空'); |
| 76 | $test->assertFalse(Comments::addLocal($thread, '記事', $alice, str_repeat('あ', Comments::MAX_LENGTH + 1))->isSuccess, '長過ぎる'); |
| 77 | $test->assertFalse(Comments::addLocal($thread, '記事', $banned, 'こんにちは')->isSuccess, 'BAN'); |
| 78 | $test->assertFalse(Comments::addLocal('../config/x', '記事', $alice, 'こんにちは')->isSuccess, '不正なスレッド'); |
| 79 | $test->assertFalse(Comments::addLocal($thread, '記事', $alice, 'こんにちは', '00000000000000000000')->isSuccess, '返信先が無い'); |
| 80 | $test->assertFalse(Comments::validThread('blog/../x')); |
| 81 | $test->assertFalse(Comments::validThread('blog')); |
| 82 | $test->assertTrue(Comments::validThread('tutorial/linalg/01-abc')); |
| 83 | $test->assertEquals(0, Comments::count($thread)); |
| 84 | }); |
| 85 | |
| 86 | $test->it('Markdownを安全なHTMLにして、管理者に通知するはず', function($test) use ($alice, $types, $thread): void { |
| 87 | $res = Comments::addLocal($thread, '記事', $alice, "**太字**と`コード` #タグ\n<script>alert(1)</script>"); |
| 88 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 89 | $c = Comments::all($thread)[$res->data['id']]; |
| 90 | $test->assertStringContains('<strong>太字</strong>', $c['html']); |
| 91 | $test->assertStringContains('<code>コード</code>', $c['html']); |
| 92 | $test->assertStringContains('<script>', $c['html']); |
| 93 | $test->assertFalse(str_contains($c['html'], '<a'), 'ハッシュタグはリンクにしない'); |
| 94 | $test->assertEquals(['comment:alice1234'], $types('admin1234')); |
| 95 | $test->assertEquals([], $types('alice1234'), '自分には通知しない'); |
| 96 | }); |
| 97 | |
| 98 | $test->it('返信は返信先の人に通知し、深さ付きで表示するはず', function($test) use ($admin, $alice, $bob, $types, $thread): void { |
| 99 | $first = array_key_first(Comments::all($thread)); |
| 100 | $reply = Comments::addLocal($thread, '記事', $bob, '返信です', $first)->data['id']; |
| 101 | $test->assertEquals(['reply:bob12345'], $types('alice1234')); |
| 102 | $test->assertEquals(['comment:bob12345', 'comment:alice1234'], $types('admin1234')); |
| 103 | |
| 104 | // 管理者への返信は「返信」の通知だけ |
| 105 | Comments::addLocal($thread, '記事', $admin, '管理者です', $reply); |
| 106 | $own = Comments::addLocal($thread, '記事', $admin, '自分のコメント')->data['id']; |
| 107 | Comments::addLocal($thread, '記事', $bob, '管理者への返信', $own); |
| 108 | $test->assertEquals(['reply:bob12345', 'comment:bob12345', 'comment:alice1234'], $types('admin1234')); |
| 109 | |
| 110 | $view = Comments::view($thread, $bob); |
| 111 | $test->assertEquals([0, 1, 2, 0, 1], array_column($view, 'depth')); |
| 112 | $test->assertEquals('<p>返信です</p>', $view[1]['html']); |
| 113 | $test->assertTrue($view[1]['canDelete'], '自分のコメントは削除出来る'); |
| 114 | $test->assertFalse($view[0]['canDelete'], '他人のコメントは削除出来ない'); |
| 115 | $test->assertFalse($view[1]['canLike'], '自分のコメントにはいいね出来ない'); |
| 116 | $test->assertTrue($view[0]['canLike']); |
| 117 | $test->assertTrue(Comments::view($thread, $admin)[0]['canDelete'], '管理者は全部削除出来る'); |
| 118 | $test->assertFalse(Comments::view($thread, null)[0]['canReply'], 'ログインしていなければ返信出来ない'); |
| 119 | $test->assertEquals(5, Comments::count($thread)); |
| 120 | }); |
| 121 | |
| 122 | $test->it('いいねは切り替えで、通知は重複しないはず', function($test) use ($alice, $bob, $types, $thread): void { |
| 123 | $first = array_key_first(Comments::all($thread)); |
| 124 | $test->assertTrue(Comments::like($thread, $first, $bob)->data['liked']); |
| 125 | $test->assertFalse(Comments::like($thread, $first, $bob)->data['liked']); |
| 126 | $test->assertTrue(Comments::like($thread, $first, $bob)->data['liked']); |
| 127 | $test->assertEquals(['like:bob12345', 'reply:bob12345'], $types('alice1234')); |
| 128 | $test->assertFalse(Comments::like($thread, $first, $alice)->isSuccess, '自分のコメント'); |
| 129 | $test->assertFalse(Comments::like($thread, 'nai', $bob)->isSuccess); |
| 130 | $v = Comments::view($thread, $bob)[0]; |
| 131 | $test->assertEquals(1, $v['likes']); |
| 132 | $test->assertTrue($v['liked']); |
| 133 | }); |
| 134 | |
| 135 | $test->it('通知を既読に出来るはず', function($test) use ($thread): void { |
| 136 | $test->assertEquals(2, Notifications::unreadCount('alice1234')); |
| 137 | $n = Comments::present(Notifications::list('alice1234')[0]); |
| 138 | $test->assertStringContains('いいね', $n['label']); |
| 139 | $test->assertStringContains("/{$thread}#c-", $n['page']); |
| 140 | Notifications::markRead('alice1234'); |
| 141 | $test->assertEquals(0, Notifications::unreadCount('alice1234')); |
| 142 | $test->assertEquals(2, count(Notifications::list('alice1234'))); |
| 143 | }); |
| 144 | |
| 145 | $test->it('返信のあるコメントは中身だけ消し、返信が無くなれば完全に消すはず', function($test) use ($admin, $alice, $bob, $thread): void { |
| 146 | $ids = array_keys(Comments::all($thread)); |
| 147 | $test->assertFalse(Comments::delete($thread, $ids[0], $bob)->isSuccess, '他人のコメントは消せない'); |
| 148 | $test->assertTrue(Comments::delete($thread, $ids[0], $alice)->isSuccess); |
| 149 | $c = Comments::all($thread)[$ids[0]]; |
| 150 | $test->assertTrue($c['deleted']); |
| 151 | $test->assertFalse(isset($c['source']) || isset($c['html']), '本文は残さない'); |
| 152 | $test->assertEquals(4, Comments::count($thread)); |
| 153 | $test->assertTrue(Comments::view($thread, null)[0]['deleted']); |
| 154 | $test->assertFalse(Comments::addLocal($thread, '記事', $bob, '返信', $ids[0])->isSuccess, '削除されたコメントには返信出来ない'); |
| 155 | |
| 156 | // 返信の返信(管理者)→返信(bob)の順に消すと、中身を消したaliceのコメントも消える |
| 157 | $test->assertTrue(Comments::delete($thread, $ids[1], $bob)->isSuccess); |
| 158 | $test->assertTrue(Comments::all($thread)[$ids[1]]['deleted']); |
| 159 | $test->assertTrue(Comments::delete($thread, $ids[2], $admin)->isSuccess); |
| 160 | $left = Comments::all($thread); |
| 161 | $test->assertFalse(isset($left[$ids[0]]) || isset($left[$ids[1]]) || isset($left[$ids[2]])); |
| 162 | $test->assertEquals(2, count($left)); |
| 163 | }); |
| 164 | }); |
| 165 | |
| 166 | $test->describe('Fediverseからの返信', function($test) use ($admin, $alice, $types): void { |
| 167 | if (!Comments::enabled() || !ACTIVITYPUB_ENABLED) $test->skipAll('COMMENTS_ENABLED又はACTIVITYPUB_ENABLEDが無効'); |
| 168 | |
| 169 | $post = ['title' => '英語の記事', 'date' => '2025-01-01 10:00', 'thumbnail' => '', 'category' => [], 'uuid' => 'c0ffee00-0000-4000-8000-000000000001', |
| 170 | 'preview' => '本文', 'slug' => 'english-post', 'section' => 'enblog']; |
| 171 | $ap = new Activitypub([$post]); |
| 172 | $target = (string)$ap->objectId($post); |
| 173 | $carol = 'https://1.1.1.1/users/carol'; |
| 174 | Remote::cacheActor(['id' => $carol, 'type' => 'Person', 'preferredUsername' => 'carol', 'name' => 'Carol <b>C</b>', |
| 175 | 'url' => 'https://1.1.1.1/@carol', 'inbox' => 'https://1.1.1.1/users/carol/inbox', |
| 176 | 'icon' => ['type' => 'Image', 'url' => 'https://1.1.1.1/carol.png']]); |
| 177 | $note = fn(string $id, string $inReplyTo, array $extra = []) => $extra + [ |
| 178 | 'id' => "https://1.1.1.1/users/carol/statuses/{$id}", 'type' => 'Note', 'attributedTo' => $carol, 'inReplyTo' => $inReplyTo, |
| 179 | 'to' => [Fedi::PUBLIC], 'cc' => [], |
| 180 | 'content' => '<p><span class="h-card"><a href="'.Fedi::base().'" class="u-url mention">@<span>'.FEDIINFO['actor'].'</span></a></span> 面白い記事!<script>x</script></p>', |
| 181 | ]; |
| 182 | $create = fn(array $obj) => ['type' => 'Create', 'actor' => $carol, 'object' => $obj]; |
| 183 | $thread = 'enblog/english-post'; |
| 184 | |
| 185 | $test->it('ブログ毎に違うNoteのIDのはず', function($test) use ($post, $target): void { |
| 186 | $test->assertStringContains('/ap/objects/enblog/'.$post['uuid'], $target); |
| 187 | $test->assertNull((new Activitypub([$post]))->objectId(['section' => 'enblog'] + ['uuid' => 'nai'] + $post)); |
| 188 | $blog = ['section' => 'blog'] + $post; |
| 189 | $test->assertStringContains('/ap/objects/'.$post['uuid'], (string)(new Activitypub([$blog]))->objectId($blog)); |
| 190 | }); |
| 191 | |
| 192 | $test->it('記事への返信をコメントにして、管理者に通知するはず', function($test) use ($ap, $note, $create, $target, $thread, $carol, $types): void { |
| 193 | $test->assertTrue($ap->handleComments($create($note('1', $target)))); |
| 194 | $all = Comments::all($thread); |
| 195 | $test->assertEquals(1, count($all)); |
| 196 | $c = array_values($all)[0]; |
| 197 | $test->assertEquals('<p>面白い記事!</p>', $c['html'], 'ブログのアクターへのメンションとscriptは消す'); |
| 198 | $test->assertEquals('@carol@1.1.1.1', $c['author']['handle']); |
| 199 | $test->assertEquals('Carol C', $c['author']['name']); |
| 200 | $test->assertEquals('https://1.1.1.1/@carol', $c['author']['url']); |
| 201 | $test->assertEquals("comment:{$carol}", $types('admin1234')[0]); |
| 202 | $test->assertEquals('英語の記事', Notifications::list('admin1234')[0]['title']); |
| 203 | // 再送されても1つ |
| 204 | $test->assertTrue($ap->handleComments($create($note('1', $target)))); |
| 205 | $test->assertEquals(1, Comments::count($thread)); |
| 206 | }); |
| 207 | |
| 208 | $test->it('関係の無い・非公開・偽装した返信は無視するはず', function($test) use ($ap, $note, $create, $target, $thread, $carol): void { |
| 209 | $test->assertFalse($ap->handleComments($create($note('2', 'https://8.8.8.8/notes/1'))), '他の投稿への返信'); |
| 210 | $test->assertFalse($ap->handleComments($create($note('3', $target, ['to' => [$carol.'/followers']]))), 'フォロワー限定'); |
| 211 | $test->assertFalse($ap->handleComments($create($note('4', $target, ['to' => [Fedi::blogActorUrl()]]))), 'DM'); |
| 212 | $test->assertFalse($ap->handleComments($create($note('5', $target, ['attributedTo' => 'https://1.1.1.1/users/dave']))), '作者が違う'); |
| 213 | $test->assertFalse($ap->handleComments($create($note('6', $target, ['id' => 'https://8.8.8.8/notes/6']))), '他のサーバーのID'); |
| 214 | $test->assertFalse($ap->handleComments(['type' => 'Like', 'actor' => $carol, 'object' => $target])); |
| 215 | $test->assertEquals(1, Comments::count($thread)); |
| 216 | }); |
| 217 | |
| 218 | $test->it('リモートのコメントへの返信と、ローカルのユーザーの返信を繋げるはず', function($test) use ($ap, $note, $create, $thread, $alice, $carol, $types): void { |
| 219 | $first = array_key_first(Comments::all($thread)); |
| 220 | $mine = Comments::addLocal($thread, '英語の記事', $alice, 'Carolさんへ', $first)->data['id']; |
| 221 | $test->assertTrue($ap->handleComments($create($note('7', 'https://1.1.1.1/users/carol/statuses/1')))); |
| 222 | $view = Comments::view($thread, $alice); |
| 223 | $test->assertEquals([0, 1, 1], array_column($view, 'depth')); |
| 224 | $test->assertTrue($view[0]['who']['remote']); |
| 225 | // リモートの画像は直接読み込まない(マルチユーザーではこのサイト経由) |
| 226 | if (Fedi::enabled()) $test->assertStringContains('/microblog/media?u=', $view[0]['who']['avatar']); |
| 227 | else $test->assertEquals(ass_url('user/noicon.png'), $view[0]['who']['avatar']); |
| 228 | $test->assertTrue($view[0]['canLike'], 'リモートのコメントにもいいね出来る'); |
| 229 | $test->assertFalse(in_array("reply:{$carol}", $types('alice1234'), true), 'aliceへの返信ではない'); |
| 230 | $test->assertEquals(["comment:{$carol}", 'comment:alice1234'], array_slice($types('admin1234'), 0, 2), '管理者には全部のコメント'); |
| 231 | }); |
| 232 | |
| 233 | $test->it('返信のコレクションはFediverseからの記事への直接の返信だけのはず', function($test) use ($ap, $post, $thread, $target): void { |
| 234 | $json = json_decode($ap->getReplies($post['uuid'], 'enblog'), true); |
| 235 | $test->assertEquals("{$target}/replies", $json['id']); |
| 236 | $test->assertEquals('OrderedCollection', $json['type']); |
| 237 | $test->assertEquals(['https://1.1.1.1/users/carol/statuses/1'], $json['orderedItems'], 'ローカルのコメントと返信への返信は含まない'); |
| 238 | $test->assertEquals(1, $json['totalItems']); |
| 239 | $test->assertEquals('', $ap->getReplies($post['uuid']), 'blogには無い'); |
| 240 | $test->assertEquals(['https://1.1.1.1/users/carol/statuses/7'], Comments::fediReplies($thread, array_key_first(Comments::all($thread)))); |
| 241 | }); |
| 242 | |
| 243 | $test->it('本人の編集と削除だけを反映するはず', function($test) use ($ap, $note, $thread, $carol): void { |
| 244 | $edited = $note('1', '', ['content' => '<p>編集しました</p>']); |
| 245 | $test->assertFalse($ap->handleComments(['type' => 'Update', 'actor' => 'https://1.1.1.1/users/dave', 'object' => $edited])); |
| 246 | $test->assertTrue($ap->handleComments(['type' => 'Update', 'actor' => $carol, 'object' => $edited])); |
| 247 | $c = array_values(Comments::all($thread))[0]; |
| 248 | $test->assertEquals('<p>編集しました</p>', $c['html']); |
| 249 | $test->assertTrue(isset($c['edited'])); |
| 250 | |
| 251 | $test->assertFalse($ap->handleComments(['type' => 'Delete', 'actor' => 'https://1.1.1.1/users/dave', 'object' => 'https://1.1.1.1/users/carol/statuses/7'])); |
| 252 | $test->assertTrue($ap->handleComments(['type' => 'Delete', 'actor' => $carol, 'object' => ['id' => 'https://1.1.1.1/users/carol/statuses/7', 'type' => 'Tombstone']])); |
| 253 | $test->assertEquals(2, Comments::count($thread)); |
| 254 | }); |
| 255 | |
| 256 | $test->it('アカウントの削除で全部の返信を消すはず', function($test) use ($ap, $thread, $carol): void { |
| 257 | $test->assertTrue($ap->handleComments(['type' => 'Delete', 'actor' => $carol, 'object' => $carol])); |
| 258 | $view = Comments::view($thread, null); |
| 259 | // 返信のあるコメントは「削除されました」として残る |
| 260 | $test->assertEquals([true, false], array_column($view, 'deleted')); |
| 261 | $test->assertNull(Comments::findRemote('https://1.1.1.1/users/carol/statuses/1')); |
| 262 | }); |
| 263 | }); |
| 264 | |
| 265 | $test->describe('Fediverseのロールを持つユーザーのコメント', function($test) use ($admin, $alice, $bob, $tmp): void { |
| 266 | if (!Comments::enabled() || !ACTIVITYPUB_MULTIUSER) { |
| 267 | $test->skipAll('COMMENTS_ENABLED又はマルチユーザーモードが無効'); |
| 268 | } else if (!is_readable((string)FEDIINFO['pubkey'])) { |
| 269 | $test->skipAll('ブログのアカウントの公開鍵(FEDIINFOのpubkey)が無い'); |
| 270 | } |
| 271 | |
| 272 | // ロールを外されたユーザー(fediDefaultがあり、FEDIVERSEのビットが無い) |
| 273 | file_put_contents("{$tmp}/user/6.carol1234.json", json_encode(['id' => 6, 'username' => 'carol1234', 'displayname' => '', 'namecolor' => '', |
| 274 | 'avatar' => '', 'gender' => -1, 'role' => \Roles::MEMBER, 'fediDefault' => true])); |
| 275 | $carol = (object)['id' => 6, 'username' => 'carol1234', 'role' => \Roles::MEMBER]; |
| 276 | $post = ['title' => '連合する記事', 'date' => '2025-01-01 10:00', 'thumbnail' => '', 'category' => [], 'uuid' => 'c0ffee00-0000-4000-8000-000000000002', |
| 277 | 'preview' => '本文', 'slug' => 'fedi-post', 'section' => 'enblog']; |
| 278 | $ap = new Activitypub([$post]); |
| 279 | $postNote = ACTIVITYPUB_MULTIUSER ? json_decode($ap->getObject($post['uuid'], 'enblog'), true) : []; |
| 280 | $thread = 'enblog/fedi-post'; |
| 281 | $noteOf = fn(string $thread, string $id) => Notes::get((string)(Fedi::localNoteId((string)(Comments::all($thread)[$id]['apId'] ?? ''))[1] ?? '')); |
| 282 | $ids = new \ArrayObject(); |
| 283 | |
| 284 | $test->it('ロールを持つユーザーのコメントは本人の投稿として記事に返信するはず', function($test) use ($ap, $post, $postNote, $thread, $alice, $noteOf, $ids): void { |
| 285 | $res = Comments::addLocal($thread, '連合する記事', $alice, 'こんにちは **世界**', null, $postNote); |
| 286 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 287 | $ids['alice'] = $res->data['id']; |
| 288 | $c = Comments::all($thread)[$ids['alice']]; |
| 289 | $test->assertStringContains(Fedi::actorUrl('alice1234').'/notes/', (string)($c['apId'] ?? '')); |
| 290 | $note = $noteOf($thread, $ids['alice']); |
| 291 | $test->assertEquals($postNote['id'], $note['inReplyTo']); |
| 292 | $test->assertEquals('public', $note['visibility']); |
| 293 | $test->assertEquals('こんにちは **世界**', $note['source']); |
| 294 | $test->assertEquals([$c['apId']], json_decode($ap->getReplies($post['uuid'], 'enblog'), true)['orderedItems']); |
| 295 | $test->assertEquals($ids['alice'], Comments::findRemote($c['apId'])[1], 'Fediverseからの返信を繋げられる'); |
| 296 | }); |
| 297 | |
| 298 | $test->it('ロールが無いユーザーと連合しない記事のコメントは送らないはず', function($test) use ($postNote, $thread, $carol, $alice, $ids): void { |
| 299 | $res = Comments::addLocal($thread, '連合する記事', $carol, 'ロール無し', $ids['alice'], $postNote); |
| 300 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 301 | $ids['carol'] = $res->data['id']; |
| 302 | $test->assertFalse(isset(Comments::all($thread)[$ids['carol']]['apId'])); |
| 303 | $id = Comments::addLocal('tutorial/linalg/01-test', '勉強会', $alice, '勉強会のコメント')->data['id']; |
| 304 | $test->assertFalse(isset(Comments::all('tutorial/linalg/01-test')[$id]['apId'])); |
| 305 | $test->assertEquals(1, count(Comments::fediReplies($thread))); |
| 306 | }); |
| 307 | |
| 308 | $test->it('返信はFediverseにある一番近い親コメントへの返信のはず', function($test) use ($postNote, $thread, $bob, $noteOf, $ids): void { |
| 309 | // carolのコメントはFediverseに無いので、その親(alice)への返信にする |
| 310 | $ids['bob'] = Comments::addLocal($thread, '連合する記事', $bob, 'carolさんへ', $ids['carol'], $postNote)->data['id']; |
| 311 | $alice = Comments::all($thread)[$ids['alice']]['apId']; |
| 312 | $test->assertEquals($alice, $noteOf($thread, $ids['bob'])['inReplyTo']); |
| 313 | $test->assertEquals([Comments::all($thread)[$ids['bob']]['apId']], Comments::fediReplies($thread, $ids['carol'])); |
| 314 | }); |
| 315 | |
| 316 | $test->it('マイクロブログで編集・削除するとコメントも変わるはず', function($test) use ($postNote, $thread, $bob, $noteOf, $ids): void { |
| 317 | $note = $noteOf($thread, $ids['bob']); |
| 318 | $test->assertTrue(Social::edit('bob12345', $note['id'], '編集しました')->isSuccess); |
| 319 | $c = Comments::all($thread)[$ids['bob']]; |
| 320 | $test->assertStringContains('編集しました', $c['html']); |
| 321 | $test->assertTrue(isset($c['edited'])); |
| 322 | $test->assertTrue(Social::delete('bob12345', $note['id'])->isSuccess); |
| 323 | $test->assertFalse(isset(Comments::all($thread)[$ids['bob']])); |
| 324 | }); |
| 325 | |
| 326 | $test->it('コメントを削除すると投稿も消えるはず', function($test) use ($thread, $alice, $admin, $noteOf, $ids): void { |
| 327 | $apId = Comments::all($thread)[$ids['alice']]['apId']; |
| 328 | $noteId = Fedi::localNoteId($apId)[1]; |
| 329 | $test->assertTrue(Comments::delete($thread, $ids['alice'], $admin)->isSuccess, '管理者が消しても本人の投稿を消す'); |
| 330 | $test->assertNull(Notes::get($noteId)); |
| 331 | $test->assertTrue(Comments::all($thread)[$ids['alice']]['deleted'], 'carolの返信があるので中身だけ消す'); |
| 332 | $test->assertNull(Comments::findRemote($apId)); |
| 333 | $test->assertEquals([], Comments::fediReplies($thread)); |
| 334 | }); |
| 335 | }); |
| 336 | |
| 337 | // 一時ディレクトリを削除する |
| 338 | $rm = function(string $dir) use (&$rm): void { |
| 339 | foreach (scandir($dir) ?: [] as $f) { |
| 340 | if ($f === '.' || $f === '..') continue; |
| 341 | is_dir("{$dir}/{$f}") ? $rm("{$dir}/{$f}") : @unlink("{$dir}/{$f}"); |
| 342 | } |
| 343 | @rmdir($dir); |
| 344 | }; |
| 345 | $rm($tmp); |
| 346 | Comments::$dataDir = Notifications::$dataDir = Fedi::$dataDir = Fedi::$userDir = null; |
| 347 | |
| 348 | $test->printSummary(); |