| 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\Auth; |
| 44 | use Std\Lib\Fedi\Fedi; |
| 45 | use Std\Lib\Fedi\Remote; |
| 46 | use Std\Lib\Git\Commit; |
| 47 | use Std\Lib\Git\Person; |
| 48 | use Std\Lib\Git\Repository; |
| 49 | use Std\Lib\Git\Tree; |
| 50 | use Std\Lib\Gitler\Archive; |
| 51 | use Std\Lib\Gitler\Federation; |
| 52 | use Std\Lib\Gitler\Gitler; |
| 53 | use Std\Lib\Gitler\Issues; |
| 54 | use Std\Lib\Gitler\Keys; |
| 55 | use Std\Lib\Gitler\Notify; |
| 56 | use Std\Lib\Gitler\Pulls; |
| 57 | use Std\Lib\Notifications; |
| 58 | use Std\Lib\Gitler\Languages; |
| 59 | use Roles; |
| 60 | |
| 61 | $test = new Tester([ |
| 62 | 'colorOutput' => true, |
| 63 | 'verboseOutput' => true |
| 64 | ]); |
| 65 | |
| 66 | // 本物のデータを触らない様に、一時ディレクトリを使う |
| 67 | $base = sys_get_temp_dir().'/lbgitler_'.bin2hex(random_bytes(6)); |
| 68 | foreach (['repos', 'user', 'keys'] as $d) mkdir("{$base}/{$d}", 0700, true); |
| 69 | $rmTree = function(string $dir) use (&$rmTree): void { |
| 70 | foreach (@scandir($dir) ?: [] as $f) { |
| 71 | if ($f === '.' || $f === '..') continue; |
| 72 | $p = "{$dir}/{$f}"; |
| 73 | if (is_dir($p) && !is_link($p)) $rmTree($p); |
| 74 | else { |
| 75 | @chmod($p, 0666); |
| 76 | @unlink($p); |
| 77 | } |
| 78 | } |
| 79 | @rmdir($dir); |
| 80 | }; |
| 81 | register_shutdown_function(fn() => $rmTree($base)); |
| 82 | Gitler::$root = "{$base}/repos"; |
| 83 | Gitler::$userDir = "{$base}/user"; |
| 84 | Keys::$dir = "{$base}/keys"; |
| 85 | |
| 86 | // ユーザー(role、fediDefaultはロールを管理者が変えた印) |
| 87 | $mkUser = function(int $id, string $name, int $role, bool $rolesSet = true) use ($base): \stdClass { |
| 88 | $u = ['id' => $id, 'username' => $name, 'email' => "{$name}@example.com", 'displayname' => '', 'role' => $role, 'password' => 'x', 'tokens' => []]; |
| 89 | if ($rolesSet) $u['fediDefault'] = true; |
| 90 | file_put_contents("{$base}/user/{$id}.{$name}.json", json_encode($u)); |
| 91 | return Gitler::user($name); |
| 92 | }; |
| 93 | $admin = $mkUser(1, 'admin001', Roles::MEMBER | Roles::ADMIN); |
| 94 | $owner = $mkUser(2, 'owner001', Roles::MEMBER | Roles::GIT_REPOS); |
| 95 | $other = $mkUser(3, 'other001', Roles::MEMBER, false); |
| 96 | $plain = $mkUser(4, 'plain001', Roles::MEMBER); |
| 97 | $banned = $mkUser(5, 'banned01', Roles::BANNED); |
| 98 | $pulls = $mkUser(6, 'pulls001', Roles::MEMBER | Roles::GIT_PULLS | Roles::GIT_ISSUES); |
| 99 | |
| 100 | // 公開鍵(ed25519、ecdsa、RSA) |
| 101 | $str = fn(string $s) => pack('N', strlen($s)).$s; |
| 102 | $ed = fn() => 'ssh-ed25519 '.base64_encode($str('ssh-ed25519').$str(random_bytes(32))); |
| 103 | $rsa = function(int $bits) use ($str): string { |
| 104 | $n = chr(0xc0).random_bytes(intdiv($bits, 8) - 1); |
| 105 | return 'ssh-rsa '.base64_encode($str('ssh-rsa').$str("\x01\x00\x01").$str("\0".$n)); |
| 106 | }; |
| 107 | |
| 108 | |
| 109 | $test->describe('Gitler(権限)', function($test) use ($admin, $owner, $other, $plain, $banned, $pulls): void { |
| 110 | $test->it('ロールで使える機能が決まるはず', function($test) use ($admin, $owner, $other, $plain, $banned, $pulls): void { |
| 111 | $can = fn(?\stdClass $u) => [Gitler::userCan($u, 'repos'), Gitler::userCan($u, 'issues'), Gitler::userCan($u, 'pulls')]; |
| 112 | $test->assertSame([true, true, true], $can($admin)); |
| 113 | $test->assertSame([true, false, false], $can($owner)); |
| 114 | $test->assertSame([false, true, false], $can($other), 'ロールを変えていないユーザーはイシューだけ(既定)'); |
| 115 | $test->assertSame([false, false, false], $can($plain)); |
| 116 | $test->assertSame([false, false, false], $can($banned)); |
| 117 | $test->assertSame([false, true, true], $can($pulls)); |
| 118 | $test->assertSame([false, false, false], $can(null)); |
| 119 | }); |
| 120 | |
| 121 | $test->it('リポジトリの名前とディレクトリを確かめるはず', function($test): void { |
| 122 | foreach (['repo', 'my-repo_1.0', 'A', '_x'] as $ok) $test->assertTrue(Gitler::validName($ok), $ok); |
| 123 | foreach (['', '.', '..', 'a..b', 'x.git', 'X.GIT', '-x', '.x', 'a/b', 'a b', str_repeat('a', 101), "a\0"] as $bad) $test->assertFalse(Gitler::validName($bad), $bad); |
| 124 | $test->assertFalse(Gitler::validOwner('a-b')); |
| 125 | Gitler::$root = null; |
| 126 | $root = str_replace('\\', '/', ROOT); |
| 127 | $test->assertSame(dirname($root).'/git', Gitler::root(), '既定は../git(ROOTから)'); |
| 128 | $test->assertSame(dirname($root, 3).'/git', Gitler::resolvePath('../../../git')); |
| 129 | $test->assertSame('/var/www/git', Gitler::resolvePath('/var/www/htdocs/example.com/www/../../../git')); |
| 130 | $test->assertSame('/git', Gitler::resolvePath('/htdocs/example.com/www/../../../git'), 'chrootの中'); |
| 131 | $test->assertSame('/a/b', Gitler::resolvePath('/a/./b/')); |
| 132 | }); |
| 133 | }); |
| 134 | |
| 135 | $test->describe('Gitler(リポジトリ)', function($test) use ($base, $admin, $owner, $other, $plain, $banned): void { |
| 136 | Gitler::$root = "{$base}/repos"; |
| 137 | |
| 138 | $test->it('ロールが有ればリポジトリを作れるはず', function($test) use ($owner, $plain): void { |
| 139 | $test->assertFalse(Gitler::create($plain, 'nope')->isSuccess); |
| 140 | $res = Gitler::create($owner, 'public1', ['description' => '公開', 'readme' => true]); |
| 141 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 142 | $test->assertTrue(Gitler::exists('owner001', 'public1')); |
| 143 | $repo = Gitler::open('owner001', 'public1'); |
| 144 | $test->assertSame("# public1\n\n公開\n", $repo->blob($repo->resolve('master:README.md'))); |
| 145 | $test->assertSame('owner001', $repo->commit($repo->resolve('master'))->author->name); |
| 146 | $test->assertSame('group', $repo->config->get('core.sharedrepository')); |
| 147 | $test->assertSame("公開\n", file_get_contents($repo->path.'/description')); |
| 148 | $test->assertFalse(Gitler::create($owner, 'public1')->isSuccess, '同じ名前'); |
| 149 | $test->assertFalse(Gitler::create($owner, 'bad.git')->isSuccess); |
| 150 | $res = Gitler::create($owner, 'secret', ['private' => true, 'objectFormat' => 'sha256', 'branch' => 'trunk']); |
| 151 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 152 | $repo = Gitler::open('owner001', 'secret'); |
| 153 | $test->assertSame('sha256', $repo->hashAlgo); |
| 154 | $test->assertSame('refs/heads/trunk', $repo->headRef()); |
| 155 | $test->assertSame('refs/heads/master', Gitler::open('owner001', 'public1')->headRef(), '既定のブランチはmaster'); |
| 156 | $test->assertTrue($repo->isEmpty()); |
| 157 | $names = array_map(fn($r) => $r['name'], Gitler::list('owner001')); |
| 158 | sort($names); |
| 159 | $test->assertSame(['public1', 'secret'], $names); |
| 160 | }); |
| 161 | |
| 162 | $test->it('公開・非公開と共同作業者で権限が決まるはず', function($test) use ($admin, $owner, $other, $plain, $banned): void { |
| 163 | $a = fn(?\stdClass $u, string $name) => Gitler::access($u, 'owner001', $name); |
| 164 | $test->assertSame(Gitler::READ, $a(null, 'public1')); |
| 165 | $test->assertSame(Gitler::NONE, $a(null, 'secret')); |
| 166 | $test->assertSame(Gitler::ADMIN, $a($owner, 'secret')); |
| 167 | $test->assertSame(Gitler::ADMIN, $a($admin, 'secret')); |
| 168 | $test->assertSame(Gitler::NONE, $a($other, 'secret')); |
| 169 | $test->assertSame(Gitler::READ, $a($other, 'public1')); |
| 170 | $test->assertSame(Gitler::NONE, $a($banned, 'secret')); |
| 171 | $test->assertSame(Gitler::NONE, $a($owner, 'nothing')); |
| 172 | $meta = Gitler::meta('owner001', 'secret'); |
| 173 | $meta['collaborators'] = ['other001' => 'read', 'plain001' => 'write', 'banned01' => 'write']; |
| 174 | Gitler::saveMeta('owner001', 'secret', $meta); |
| 175 | $test->assertSame(Gitler::READ, $a($other, 'secret')); |
| 176 | $test->assertSame(Gitler::WRITE, $a($plain, 'secret'), '書ける共同作業者はロールが無くても良い'); |
| 177 | $test->assertSame(Gitler::NONE, $a($banned, 'secret'), 'BANされたユーザーは共同作業者でも駄目'); |
| 178 | // 所有者がロールを失うと読むだけ |
| 179 | $lost = clone $owner; |
| 180 | $lost->role = Roles::MEMBER; |
| 181 | $test->assertSame(Gitler::READ, $a($lost, 'secret')); |
| 182 | }); |
| 183 | |
| 184 | $test->it('名前を変えて、消せるはず', function($test) use ($owner): void { |
| 185 | $test->assertTrue(Gitler::create($owner, 'tmp1')->isSuccess); |
| 186 | $test->assertFalse(Gitler::rename('owner001', 'tmp1', 'public1')->isSuccess, '有る名前'); |
| 187 | $test->assertFalse(Gitler::rename('owner001', 'tmp1', '../x')->isSuccess); |
| 188 | $test->assertTrue(Gitler::rename('owner001', 'tmp1', 'tmp2')->isSuccess); |
| 189 | $test->assertFalse(Gitler::exists('owner001', 'tmp1')); |
| 190 | $test->assertTrue(Gitler::delete('owner001', 'tmp2')); |
| 191 | $test->assertFalse(Gitler::exists('owner001', 'tmp2')); |
| 192 | $test->assertFalse(is_dir(Gitler::root().'/owner001/tmp2.git')); |
| 193 | }); |
| 194 | |
| 195 | $test->it('全部のリポジトリを整理出来るはず', function($test): void { |
| 196 | $res = Gitler::gcAll(); |
| 197 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 198 | $repo = Gitler::open('owner001', 'public1'); |
| 199 | $test->assertCount(1, $repo->objects->packs(true)); |
| 200 | $test->assertStringContains('公開', $repo->blob($repo->resolve('master:README.md'))); |
| 201 | }); |
| 202 | }); |
| 203 | |
| 204 | $test->describe('Gitler(SSHの鍵とトークン)', function($test) use ($ed, $rsa, $str): void { |
| 205 | $test->it('公開鍵を解析して指紋を出すはず', function($test) use ($ed, $rsa, $str): void { |
| 206 | $k = Keys::parse($ed().' me@pc'); |
| 207 | $test->assertSame('ssh-ed25519', $k['type']); |
| 208 | $test->assertSame('me@pc', $k['comment']); |
| 209 | $test->assertSame('SHA256:'.rtrim(base64_encode(hash('sha256', base64_decode($k['key']), true)), '='), $k['fingerprint']); |
| 210 | $test->assertSame(4096, Keys::parse($rsa(4096))['bits']); |
| 211 | $test->assertNull(Keys::parse($rsa(1024)), '短いRSAの鍵'); |
| 212 | $test->assertNotNull(Keys::parse('ecdsa-sha2-nistp256 '.base64_encode($str('ecdsa-sha2-nistp256').$str('nistp256').$str("\x04".random_bytes(64))))); |
| 213 | // 種類が中身と違う、壊れた、未対応 |
| 214 | $test->assertNull(Keys::parse('ssh-rsa '.substr($ed(), 12))); |
| 215 | $test->assertNull(Keys::parse('ssh-ed25519 '.base64_encode($str('ssh-ed25519').$str('short')))); |
| 216 | $test->assertNull(Keys::parse('ssh-dss AAAAB3NzaC1kc3MAAACBAP')); |
| 217 | $test->assertNull(Keys::parse('from="x" ssh-ed25519 AAAA')); |
| 218 | $test->assertNull(Keys::parse('')); |
| 219 | }); |
| 220 | |
| 221 | $test->it('鍵を追加・検索・削除出来て、同じ鍵は2回登録出来ないはず', function($test) use ($ed): void { |
| 222 | $key = $ed(); |
| 223 | $res = Keys::addKey('owner001', $key.' laptop', ''); |
| 224 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 225 | $fp = $res->data['fingerprint']; |
| 226 | $test->assertSame('laptop', Keys::load('owner001')['keys'][0]['title']); |
| 227 | $test->assertFalse(Keys::addKey('other001', $key, 'x')->isSuccess, '他のユーザーでも同じ鍵は駄目'); |
| 228 | [$type, $b64] = explode(' ', $key); |
| 229 | [$user, $found] = Keys::find($type, $b64); |
| 230 | $test->assertSame('owner001', $user); |
| 231 | $test->assertSame($fp, $found['fingerprint']); |
| 232 | $line = Keys::authorizedKey($user, $found); |
| 233 | $test->assertStringContains('restrict,command="', $line); |
| 234 | $test->assertStringContains('/gitler.php serve owner001 '.$fp.'"', $line); |
| 235 | $test->assertTrue(str_ends_with($line, " {$type} {$b64}")); |
| 236 | $test->assertSame(['owner001'], array_keys(Keys::all())); |
| 237 | $test->assertTrue(Keys::removeKey('owner001', $fp)); |
| 238 | $test->assertNull(Keys::find($type, $b64)); |
| 239 | $test->assertFalse(Keys::removeKey('owner001', $fp)); |
| 240 | }); |
| 241 | |
| 242 | $test->it('アクセストークンを作って確かめられるはず', function($test): void { |
| 243 | $rw = Keys::addToken('owner001', 'ノートPC', true); |
| 244 | $ro = Keys::addToken('owner001', '読むだけ', false); |
| 245 | $test->assertTrue($rw->isSuccess && $ro->isSuccess); |
| 246 | $test->assertTrue(str_starts_with($rw->data['token'], 'lbg_')); |
| 247 | $test->assertTrue(Keys::checkToken('owner001', $rw->data['token'])['write']); |
| 248 | $test->assertFalse(Keys::checkToken('owner001', $ro->data['token'])['write']); |
| 249 | $test->assertNull(Keys::checkToken('other001', $rw->data['token']), '他のユーザーのトークン'); |
| 250 | $test->assertNull(Keys::checkToken('owner001', 'lbg_'.str_repeat('0', 40))); |
| 251 | $test->assertFalse(str_contains((string)json_encode(Keys::load('owner001')), $rw->data['token']), '平文は保存しない'); |
| 252 | $test->assertTrue(Keys::removeToken('owner001', $rw->data['id'])); |
| 253 | $test->assertNull(Keys::checkToken('owner001', $rw->data['token'])); |
| 254 | $test->assertFalse(Keys::addToken('owner001', '', true)->isSuccess); |
| 255 | }); |
| 256 | }); |
| 257 | |
| 258 | $test->describe('Gitler(アーカイブ)', function($test) use ($base): void { |
| 259 | $repo = Repository::init("{$base}/archive.git"); |
| 260 | $me = new Person('諏訪子', 's@x', 1700000000, '+0900'); |
| 261 | $long = str_repeat('長いディレクトリ/', 12).'ファイル.txt'; |
| 262 | $files = ['README.md' => "# テスト\n", 'bin/run.sh' => "#!/bin/sh\necho hi\n", 'big.bin' => random_bytes(70000), $long => "長い\n", 'empty.txt' => '']; |
| 263 | $changes = []; |
| 264 | foreach ($files as $p => $data) $changes[$p] = [$p === 'bin/run.sh' ? Tree::EXEC : Tree::FILE, $data]; |
| 265 | $changes['link'] = [Tree::LINK, 'README.md']; |
| 266 | $commit = $repo->writeCommit(Commit::create($repo->updateTree(null, $changes), [], $me, $me, "x\n")); |
| 267 | $repo->refs->update('refs/heads/main', $commit); |
| 268 | |
| 269 | $test->it('tarに全部のファイルを入れるはず(長いパス、実行、リンク、gzip)', function($test) use ($repo, $commit, $files, $long): void { |
| 270 | foreach (['tar', 'tar.gz'] as $fmt) { |
| 271 | $s = ''; |
| 272 | Archive::write($repo, $commit, $fmt, 'test-1/', function($b) use (&$s): void { $s .= $b; }); |
| 273 | if ($fmt === 'tar.gz') $s = (string)gzdecode($s); |
| 274 | // tarを読む |
| 275 | $got = []; |
| 276 | $modes = []; |
| 277 | $links = []; |
| 278 | $pax = null; |
| 279 | for ($pos = 0; $pos + 512 <= strlen($s);) { |
| 280 | $h = substr($s, $pos, 512); |
| 281 | if (trim($h, "\0") === '') break; |
| 282 | $sum = array_sum(array_map('ord', str_split(substr($h, 0, 148).' '.substr($h, 156)))); |
| 283 | $test->assertSame($sum, octdec(trim(substr($h, 148, 8), "\0 ")), 'チェックサム'); |
| 284 | $name = rtrim(substr($h, 0, 100), "\0"); |
| 285 | $prefix = rtrim(substr($h, 345, 155), "\0"); |
| 286 | $size = octdec(trim(substr($h, 124, 12), "\0 ")); |
| 287 | $type = $h[156]; |
| 288 | $data = substr($s, $pos + 512, $size); |
| 289 | $pos += 512 + (int)ceil($size / 512) * 512; |
| 290 | if ($type === 'x') { |
| 291 | preg_match('/ path=([^\n]*)\n/', $data, $m); |
| 292 | $pax = $m[1]; |
| 293 | continue; |
| 294 | } |
| 295 | $path = $pax ?? ($prefix !== '' ? $prefix.'/'.$name : $name); |
| 296 | $pax = null; |
| 297 | $modes[$path] = octdec(trim(substr($h, 100, 8), "\0 ")); |
| 298 | if ($type === '2') $links[$path] = rtrim(substr($h, 157, 100), "\0"); |
| 299 | else if ($type === '0') $got[$path] = $data; |
| 300 | } |
| 301 | foreach ($files as $p => $data) $test->assertSame(bin2hex($data), bin2hex($got['test-1/'.$p] ?? 'なし'), "{$fmt}: {$p}"); |
| 302 | $test->assertSame(0775, $modes['test-1/bin/run.sh']); |
| 303 | $test->assertSame(0664, $modes['test-1/README.md']); |
| 304 | $test->assertSame(['test-1/link' => 'README.md'], $links); |
| 305 | $test->assertTrue(isset($modes['test-1/bin/']), 'ディレクトリ'); |
| 306 | $test->assertTrue(strlen('test-1/'.$long) > 155); |
| 307 | } |
| 308 | }); |
| 309 | |
| 310 | $test->it('zipに全部のファイルを入れるはず', function($test) use ($repo, $commit, $files): void { |
| 311 | $s = ''; |
| 312 | Archive::write($repo, 'HEAD', 'zip', 'test-1/', function($b) use (&$s): void { $s .= $b; }); |
| 313 | // 中央ディレクトリから読む |
| 314 | $end = strrpos($s, "PK\x05\x06"); |
| 315 | $e = unpack('Vsig/vdisk/vcdisk/vcount/vtotal/Vsize/Voffset', $s, $end); |
| 316 | $got = []; |
| 317 | $p = $e['offset']; |
| 318 | for ($i = 0; $i < $e['total']; $i++) { |
| 319 | $c = unpack('Vsig/vmade/vneed/vflag/vmethod/vtime/vdate/Vcrc/Vcsize/Vusize/vnlen/velen/vclen/vdisk/vint/Vext/Vlocal', $s, $p); |
| 320 | $name = substr($s, $p + 46, $c['nlen']); |
| 321 | $p += 46 + $c['nlen'] + $c['elen'] + $c['clen']; |
| 322 | $l = unpack('Vsig/vneed/vflag/vmethod/vtime/vdate/Vcrc/Vcsize/Vusize/vnlen/velen', $s, $c['local']); |
| 323 | $raw = substr($s, $c['local'] + 30 + $l['nlen'] + $l['elen'], $c['csize']); |
| 324 | $data = $c['method'] === 8 ? (string)gzinflate($raw) : $raw; |
| 325 | $test->assertSame($c['crc'], crc32($data), $name); |
| 326 | $test->assertSame(0x0800, $c['flag'] & 0x0800, 'UTF-8'); |
| 327 | $got[$name] = [$data, $c['ext'] >> 16]; |
| 328 | } |
| 329 | foreach ($files as $path => $data) $test->assertSame(bin2hex($data), bin2hex($got['test-1/'.$path][0] ?? 'なし'), $path); |
| 330 | $test->assertSame(0100755, $got['test-1/bin/run.sh'][1]); |
| 331 | $test->assertSame(0120777, $got['test-1/link'][1]); |
| 332 | $test->assertSame('README.md', $got['test-1/link'][0]); |
| 333 | if (class_exists('ZipArchive')) { |
| 334 | file_put_contents(sys_get_temp_dir().'/lbgitler-test.zip', $s); |
| 335 | $z = new \ZipArchive(); |
| 336 | $test->assertTrue($z->open(sys_get_temp_dir().'/lbgitler-test.zip', \ZipArchive::CHECKCONS) === true); |
| 337 | $z->close(); |
| 338 | @unlink(sys_get_temp_dir().'/lbgitler-test.zip'); |
| 339 | } |
| 340 | }); |
| 341 | }); |
| 342 | |
| 343 | $test->describe('Gitler(言語の割合)', function($test) use ($base): void { |
| 344 | $repo = Repository::init("{$base}/langs.git"); |
| 345 | $me = new Person('諏訪子', 's@x', 1700000000, '+0900'); |
| 346 | $files = [ |
| 347 | 'src/main.c' => str_repeat('c', 600), 'src/util.h' => "int f(void);\n".str_repeat('h', 87), 'src/app.hpp' => "class A {};\n", |
| 348 | 'lib/x.h' => "namespace x {}\n".str_repeat('p', 86), 'index.php' => str_repeat('p', 300), 'style.css' => str_repeat('s', 100), |
| 349 | 'view/a.maron' => str_repeat('m', 50), 'build' => "#!/usr/bin/env -S php-8.5\n".str_repeat('p', 74), 'run' => "#!/bin/sh\necho\n", |
| 350 | 'Makefile' => "all:\n", 'README.md' => str_repeat('r', 5000), 'data.json' => str_repeat('j', 5000), 'logo.png' => "\x89PNG\0\0".random_bytes(100), |
| 351 | 'vendor/lib.js' => str_repeat('v', 5000), 'node_modules/a/b.js' => str_repeat('v', 5000), 'docs/x.py' => str_repeat('d', 5000), |
| 352 | 'app.min.js' => str_repeat('v', 5000), 'gen/x.pb.go' => str_repeat('g', 5000), 'empty' => "", |
| 353 | 'third/keep.js' => str_repeat('k', 40), 'tools/x.txt' => str_repeat('t', 20), 'shaders/a.fs' => "#version 450\nvoid main() {}\n", |
| 354 | ]; |
| 355 | $changes = []; |
| 356 | foreach ($files as $p => $data) $changes[$p] = [in_array($p, ['build', 'run'], true) ? Tree::EXEC : Tree::FILE, $data]; |
| 357 | $changes['link.c'] = [Tree::LINK, 'src/main.c']; |
| 358 | $tree = $repo->updateTree(null, $changes); |
| 359 | $commit = $repo->writeCommit(Commit::create($tree, [], $me, $me, "x\n")); |
| 360 | |
| 361 | $test->it('拡張子・名前・#!・中身で言語を決め、外部のコードとドキュメントとデータは数えないはず', function($test) use ($repo, $commit): void { |
| 362 | $s = Languages::stats($repo, $commit); |
| 363 | $test->assertSame(['C' => 700, 'PHP' => 400, 'C++' => 113, 'CSS' => 100, 'Maron' => 50, 'JavaScript' => 40, 'GLSL' => 28, 'Shell' => 15, 'Makefile' => 5], $s); |
| 364 | }); |
| 365 | |
| 366 | $test->it('.gitattributesで変えられるはず', function($test) use ($repo, $changes, $me): void { |
| 367 | $changes['.gitattributes'] = [Tree::FILE, "*.json linguist-detectable\nvendor/** -linguist-vendored\n/src/*.h linguist-language=cpp\n" |
| 368 | ."index.php linguist-generated=true\nthird/** linguist-vendored\n*.txt linguist-language=Python\nview/[a-z].maron linguist-documentation\n"]; |
| 369 | $commit = $repo->writeCommit(Commit::create($repo->updateTree(null, $changes), [], $me, $me, "y\n")); |
| 370 | $s = Languages::stats($repo, $commit); |
| 371 | $test->assertSame(['JSON' => 5000, 'JavaScript' => 5000, 'C' => 600, 'C++' => 213, 'PHP' => 100, 'CSS' => 100, 'GLSL' => 28, 'Python' => 20, 'Shell' => 15, 'Makefile' => 5], $s); |
| 372 | }); |
| 373 | |
| 374 | $test->it('パターンを正規表現にするはず', function($test): void { |
| 375 | $m = fn(string $pat, string $path): bool => (bool)preg_match(Languages::glob($pat), $path); |
| 376 | $test->assertTrue($m('*.c', 'a/b/x.c')); |
| 377 | $test->assertFalse($m('/*.c', 'a/x.c')); |
| 378 | $test->assertTrue($m('/*.c', 'x.c')); |
| 379 | $test->assertTrue($m('a/**/x.c', 'a/x.c')); |
| 380 | $test->assertTrue($m('a/**/x.c', 'a/b/c/x.c')); |
| 381 | $test->assertTrue($m('**/x.c', 'q/x.c')); |
| 382 | $test->assertTrue($m('a/**', 'a/b/c')); |
| 383 | $test->assertFalse($m('a/*', 'a/b/c')); |
| 384 | $test->assertTrue($m('[!a]?.c', 'bb.c')); |
| 385 | $test->assertFalse($m('[!a]?.c', 'ab.c')); |
| 386 | $test->assertFalse($m('a/', 'a/b')); |
| 387 | }); |
| 388 | |
| 389 | $test->it('保存して、一覧には既定のブランチの一番多い言語を出すはず', function($test) use ($repo, $commit, $tree): void { |
| 390 | $test->assertNull(Languages::primary($repo->path)); |
| 391 | $s = Languages::get($repo, $commit, true); |
| 392 | $test->assertSame('C', array_key_first($s)); |
| 393 | $c = json_decode((string)file_get_contents($repo->path.'/gitler-languages.json'), true); |
| 394 | $test->assertSame($tree, $c['head']); |
| 395 | $test->assertSame($s, $c['trees'][$tree]); |
| 396 | $test->assertSame(['name' => 'C', 'color' => '#555555'], Languages::primary($repo->path)); |
| 397 | $p = Languages::present($s); |
| 398 | $test->assertSame('C', $p[0]['name']); |
| 399 | $test->assertSame('48.2%', $p[0]['percent']); |
| 400 | $p = Languages::present(['C' => 2000, 'D' => 1]); |
| 401 | $test->assertSame(['99.950', '0.050'], array_column($p, 'width')); |
| 402 | $test->assertSame(['100.0%', '<0.1%'], array_column($p, 'percent')); |
| 403 | $many = []; |
| 404 | foreach (['C', 'Go', 'Zig', 'PHP', 'Odin', 'Hare', 'Rust', 'Lua', 'Nim', 'Shell'] as $i => $l) $many[$l] = 100 - $i; |
| 405 | $p = Languages::present($many); |
| 406 | $test->assertSame(8, count($p)); |
| 407 | $test->assertSame('その他', $p[7]['name']); |
| 408 | $test->assertSame([], Languages::present([])); |
| 409 | }); |
| 410 | }); |
| 411 | |
| 412 | $test->describe('Gitler(イシュー)', function($test) use ($owner): void { |
| 413 | Gitler::create($owner, 'issues1'); |
| 414 | $db = Issues::open('owner001', 'issues1'); |
| 415 | $me = ['user' => 'owner001']; |
| 416 | |
| 417 | $test->it('イシューとコメントを作り、探せるはず', function($test) use ($db, $me): void { |
| 418 | $test->assertFalse($db->create($me, " \n", 'x')->isSuccess, '題名が無い'); |
| 419 | $r1 = $db->create($me, "落ちる\nバグ", "手順:\n1. 開く"); |
| 420 | $test->assertTrue($r1->isSuccess); |
| 421 | $test->assertSame(1, $r1->data['number']); |
| 422 | $test->assertSame('落ちる バグ', $r1->data['issue']['title'], '改行は空白'); |
| 423 | $test->assertSame(2, $db->create(['user' => 'other001'], '機能の要望', '')->data['number']); |
| 424 | $c = $db->comment(1, ['user' => 'other001'], '再現しました'); |
| 425 | $test->assertTrue($c->isSuccess); |
| 426 | $test->assertSame(1, $c->data['comment']['id']); |
| 427 | $test->assertFalse($db->comment(1, $me, ' ')->isSuccess); |
| 428 | $test->assertFalse($db->comment(99, $me, 'x')->isSuccess); |
| 429 | $test->assertSame(['open' => 2, 'closed' => 0], $db->counts()); |
| 430 | $test->assertSame([2, 1], array_column($db->search(), 'number')); |
| 431 | $test->assertSame([1], array_column($db->search(['q' => '落ちる']), 'number')); |
| 432 | $test->assertSame([1], array_column($db->search(['q' => '#1']), 'number')); |
| 433 | $test->assertSame([2], array_column($db->search(['author' => 'other001']), 'number')); |
| 434 | $test->assertSame([1, 2], array_column($db->search(['sort' => 'comments']), 'number')); |
| 435 | $test->assertSame(1, $db->index()['issues'][1]['comments']); |
| 436 | }); |
| 437 | |
| 438 | $test->it('編集・閉じる・ラベル・担当者・マイルストーンが記録されるはず', function($test) use ($db, $me): void { |
| 439 | $test->assertTrue($db->edit(1, $me, '起動すると落ちる', '新しい本文')->isSuccess); |
| 440 | $bug = $db->saveLabel(null, 'バグ', '#ee4030', '壊れている')->data['id']; |
| 441 | $test->assertFalse($db->saveLabel(null, 'ばぐ', 'red', '')->isSuccess, '色'); |
| 442 | $test->assertFalse($db->saveLabel(null, 'バグ', '#000000', '')->isSuccess, '同じ名前'); |
| 443 | $ms = $db->saveMilestone(null, 'v1.0', '最初', '2026-12-31')->data['id']; |
| 444 | $test->assertFalse($db->saveMilestone(null, 'x', '', '2026-02-30')->isSuccess, '不正な日付'); |
| 445 | $db->triage(1, $me, [$bug, 999], ['owner001'], $ms); |
| 446 | $i = $db->get(1); |
| 447 | $test->assertSame([$bug], $i['labels'], '無いラベルは付かない'); |
| 448 | $test->assertSame(['owner001'], $i['assignees']); |
| 449 | $test->assertSame($ms, $i['milestone']); |
| 450 | $test->assertSame(['title', 'labels', 'assignees', 'milestone'], array_column($i['events'], 'type')); |
| 451 | $test->assertNotNull($db->setState(1, false, $me)); |
| 452 | $test->assertNull($db->setState(1, false, $me), '閉じたイシューは閉じない'); |
| 453 | $test->assertSame(['open' => 1, 'closed' => 1], $db->counts()); |
| 454 | $test->assertSame([1], array_column($db->search(['state' => 'closed', 'label' => $bug, 'milestone' => $ms, 'assignee' => 'owner001']), 'number')); |
| 455 | $db->deleteLabel($bug); |
| 456 | $db->deleteMilestone($ms); |
| 457 | $i = $db->get(1); |
| 458 | $test->assertSame([], $i['labels']); |
| 459 | $test->assertNull($i['milestone']); |
| 460 | $test->assertSame('新しい本文', $i['body']); |
| 461 | $test->assertSame('再現しました(編集)', $db->editComment(1, 1, '再現しました(編集)')['body']); |
| 462 | $test->assertNotNull($db->editComment(1, 1, null)); |
| 463 | $test->assertSame([], $db->get(1)['comments']); |
| 464 | $test->assertTrue($db->delete(2)); |
| 465 | $test->assertNull($db->get(2)); |
| 466 | $test->assertSame(3, $db->create($me, '次', '')->data['number'], '番号は使い回さない'); |
| 467 | }); |
| 468 | |
| 469 | $test->it('プッシュしたコミットでイシューに言及し、既定のブランチの「fixes #N」で閉じるはず', function($test) use ($db, $me): void { |
| 470 | $repo = $db->repo; |
| 471 | $db->setState(1, true, $me); |
| 472 | $p = new Person('諏訪子', 's@x', 1700000000, '+0900'); |
| 473 | $c1 = $repo->writeCommit(Commit::create($repo->updateTree(null, ['a' => [Tree::FILE, "1\n"]]), [], $p, $p, "作業中 #3\n")); |
| 474 | $repo->refs->update('refs/heads/topic', $c1); |
| 475 | $test->assertSame([], $db->processPush([['refs/heads/topic', $repo->zeroId(), $c1]], 'refs/heads/master')); |
| 476 | $test->assertSame('open', $db->get(3)['state'], '他のブランチでは閉じない'); |
| 477 | $c2 = $repo->writeCommit(Commit::create($repo->updateTree(null, ['a' => [Tree::FILE, "2\n"]]), [$c1], $p, $p, "直した\n\nFixes #1, closes #3 (see #99)\n")); |
| 478 | $repo->refs->update('refs/heads/master', $c2); |
| 479 | $test->assertSame([1, 3], $db->processPush([['refs/heads/master', $repo->zeroId(), $c2]], 'refs/heads/master')); |
| 480 | $i = $db->get(3); |
| 481 | $test->assertSame('closed', $i['state']); |
| 482 | $types = array_column($i['events'], 'type'); |
| 483 | $test->assertSame(['commit', 'commit', 'closed'], $types); |
| 484 | $test->assertSame($c2, end($i['events'])['commit']); |
| 485 | $test->assertSame([], $db->processPush([['refs/heads/master', $c1, $c2]], 'refs/heads/master'), '2回目は何もしない'); |
| 486 | }); |
| 487 | |
| 488 | $test->it('Markdownを安全にして「#3」とURLをリンクにするはず', function($test): void { |
| 489 | $h = Issues::render("[x](javascript:alert(1)) <script>alert(1)</script> #3 https://example.com/a?b=1&c=2\n\n`#4` [#5](https://x.example)", '/gitler/o/r/issues'); |
| 490 | $test->assertFalse(str_contains($h, 'javascript:')); |
| 491 | $test->assertFalse(str_contains($h, '<script')); |
| 492 | $test->assertTrue(str_contains($h, '<a href="/gitler/o/r/issues/3">#3</a>')); |
| 493 | $test->assertTrue(str_contains($h, '<a href="https://example.com/a?b=1&c=2"')); |
| 494 | $test->assertTrue(str_contains($h, '<code>#4</code>'), 'コードの中はそのまま'); |
| 495 | $test->assertFalse(str_contains($h, 'issues/5'), 'リンクの中はそのまま'); |
| 496 | $h = Issues::render('@owner001 と @nobody と a@owner001.example', '/x'); |
| 497 | $test->assertTrue(str_contains($h, '<a href="/gitler/owner001">@owner001</a>')); |
| 498 | $test->assertFalse(str_contains($h, '/gitler/nobody'), '居ないユーザー'); |
| 499 | $test->assertFalse(str_contains($h, 'gitler/owner001.example')); |
| 500 | }); |
| 501 | }); |
| 502 | |
| 503 | $test->describe('Gitler(Fediverse)', function($test) use ($base, $owner): void { |
| 504 | if (!Fedi::enabled()) { |
| 505 | $test->skipAll('マルチユーザーのFediverse(ACTIVITYPUB_MULTIUSER)が無効'); |
| 506 | return; |
| 507 | } |
| 508 | Fedi::$dataDir = "{$base}/ap"; |
| 509 | Fedi::$baseUrl = 'https://lb.test'; |
| 510 | Gitler::create($owner, 'fedi1', ['description' => 'テスト']); |
| 511 | Gitler::create($owner, 'hidden', ['private' => true]); |
| 512 | $meta = Gitler::meta('owner001', 'fedi1'); |
| 513 | $repoUrl = 'https://lb.test/gitler/owner001/fedi1'; |
| 514 | // リモートのアクター(取得しない様にキャッシュに入れる) |
| 515 | $alice = 'https://8.8.8.8/users/alice'; |
| 516 | $bob = 'https://9.9.9.9/users/bob'; |
| 517 | foreach ([$alice, $bob] as $a) { |
| 518 | Remote::cacheActor(['id' => $a, 'type' => 'Person', 'preferredUsername' => basename($a), 'name' => ucfirst(basename($a)), 'inbox' => "{$a}/inbox", |
| 519 | 'endpoints' => ['sharedInbox' => dirname($a, 2).'/inbox'], 'url' => dirname($a, 2).'/@'.basename($a)]); |
| 520 | } |
| 521 | $queue = function(): array { |
| 522 | $out = []; |
| 523 | foreach (glob(Fedi::path('queue/').'*.json') ?: [] as $f) { |
| 524 | $j = json_decode((string)file_get_contents($f), true); |
| 525 | $out[] = [$j['from'], $j['inbox'], $j['activity']['type'], $j['activity']]; |
| 526 | unlink($f); |
| 527 | } |
| 528 | return $out; |
| 529 | }; |
| 530 | |
| 531 | $test->it('WebFingerとアクターを返すはず(非公開は無し)', function($test) use ($meta, $repoUrl): void { |
| 532 | $test->assertSame('acct:owner001.fedi1@lb.test', Federation::webfinger('acct:owner001.fedi1@lb.test')['subject']); |
| 533 | $test->assertSame($repoUrl, Federation::webfinger('acct:OWNER001.FEDI1@LB.TEST')['links'][0]['href'], '大文字・小文字'); |
| 534 | $test->assertSame('acct:owner001.fedi1@lb.test', Federation::webfinger($repoUrl)['subject']); |
| 535 | $test->assertNull(Federation::webfinger('acct:owner001.hidden@lb.test')); |
| 536 | $test->assertNull(Federation::webfinger('acct:owner001.fedi1@other.example')); |
| 537 | $doc = Federation::document('owner001', 'fedi1', $meta, 'https://lb.test/gitler/owner001/fedi1.git'); |
| 538 | $test->assertSame('Service', $doc['type']); |
| 539 | $test->assertSame('owner001.fedi1', $doc['preferredUsername']); |
| 540 | $test->assertSame("{$repoUrl}/inbox", $doc['inbox']); |
| 541 | $test->assertSame($repoUrl, $doc['ticketsTrackedBy']); |
| 542 | }); |
| 543 | |
| 544 | $test->it('フォローを受け付けてAcceptを返すはず', function($test) use ($alice, $repoUrl, $queue): void { |
| 545 | $queue(); |
| 546 | [$code] = Federation::receive('owner001', 'fedi1', ['id' => "{$alice}#f1", 'type' => 'Follow', 'actor' => $alice, 'object' => $repoUrl]); |
| 547 | $test->assertSame(202, $code); |
| 548 | $test->assertTrue(isset(Federation::followers('owner001', 'fedi1')[$alice])); |
| 549 | $q = $queue(); |
| 550 | $test->assertSame([['gitler:owner001/fedi1', "{$alice}/inbox", 'Accept']], array_map(fn($x) => array_slice($x, 0, 3), $q)); |
| 551 | $test->assertSame(403, Federation::receive('owner001', 'fedi1', ['type' => 'Follow', 'actor' => $repoUrl, 'object' => $repoUrl])[0], 'ローカルを名乗る'); |
| 552 | $test->assertSame(404, Federation::receive('owner001', 'hidden', ['type' => 'Follow', 'actor' => $alice, 'object' => $repoUrl])[0], '非公開'); |
| 553 | }); |
| 554 | |
| 555 | $test->it('メンションでイシューを作り、返信をコメントにするはず', function($test) use ($alice, $bob, $repoUrl, $queue): void { |
| 556 | $mention = '<span class="h-card"><a href="'.$repoUrl.'" class="u-url mention">@<span>owner001.fedi1</span></a></span>'; |
| 557 | $note = ['id' => 'https://8.8.8.8/notes/1', 'type' => 'Note', 'attributedTo' => $alice, 'content' => "<p>{$mention} 起動しない<br />ログ:<script>x</script>何も出ない</p>", |
| 558 | 'tag' => [['type' => 'Mention', 'href' => $repoUrl]]]; |
| 559 | Federation::receive('owner001', 'fedi1', ['type' => 'Create', 'actor' => $alice, 'object' => $note]); |
| 560 | $db = Issues::open('owner001', 'fedi1'); |
| 561 | $i = $db->get(1); |
| 562 | $test->assertSame('起動しない', $i['title']); |
| 563 | $test->assertSame($alice, $i['author']['actor']); |
| 564 | $test->assertSame('Alice', $i['author']['name']); |
| 565 | $test->assertSame('@alice@8.8.8.8', $i['author']['handle']); |
| 566 | $test->assertFalse(str_contains($i['body'], 'script')); |
| 567 | $test->assertFalse(str_contains($i['body'], 'owner001.fedi1'), 'メンションは外す'); |
| 568 | $q = $queue(); |
| 569 | $test->assertSame(['Create'], array_values(array_unique(array_column($q, 2)))); |
| 570 | $test->assertSame('https://lb.test/gitler/owner001/fedi1/issues/1', $q[0][3]['object']['id']); |
| 571 | $test->assertSame([$alice], array_column($q[0][3]['object']['tag'], 'href'), '作った人をメンション'); |
| 572 | // 同じ投稿は2回作らない |
| 573 | Federation::receive('owner001', 'fedi1', ['type' => 'Create', 'actor' => $alice, 'object' => $note]); |
| 574 | $test->assertSame(1, count($db->index()['issues'])); |
| 575 | // メンションの無い投稿は無視 |
| 576 | Federation::receive('owner001', 'fedi1', ['type' => 'Create', 'actor' => $alice, 'object' => ['id' => 'https://8.8.8.8/notes/2', 'type' => 'Note', 'attributedTo' => $alice, 'content' => '<p>関係無い</p>']]); |
| 577 | $test->assertSame(1, count($db->index()['issues'])); |
| 578 | // 返信 |
| 579 | $reply = ['id' => 'https://9.9.9.9/notes/9', 'type' => 'Note', 'attributedTo' => $bob, 'inReplyTo' => "{$repoUrl}/issues/1", 'content' => "<p>{$mention} 私もです</p>"]; |
| 580 | Federation::receive('owner001', 'fedi1', ['type' => 'Create', 'actor' => $bob, 'object' => $reply]); |
| 581 | Federation::receive('owner001', 'fedi1', ['type' => 'Create', 'actor' => $bob, 'object' => $reply]); |
| 582 | $c = $db->get(1)['comments']; |
| 583 | $test->assertSame(1, count($c)); |
| 584 | $test->assertSame('<p>私もです</p>', $c[0]['body']); |
| 585 | // 返信への返信も同じイシュー |
| 586 | Federation::receive('owner001', 'fedi1', ['type' => 'Create', 'actor' => $alice, 'object' => ['id' => 'https://8.8.8.8/notes/3', 'type' => 'Note', 'attributedTo' => $alice, |
| 587 | 'inReplyTo' => $reply['id'], 'content' => '<p>ありがとう</p>']]); |
| 588 | $test->assertSame(2, count($db->get(1)['comments'])); |
| 589 | // 他人の投稿を名乗る |
| 590 | $test->assertSame(403, Federation::receive('owner001', 'fedi1', ['type' => 'Create', 'actor' => $alice, 'object' => ['id' => 'https://9.9.9.9/notes/10', 'type' => 'Note', |
| 591 | 'attributedTo' => $bob, 'inReplyTo' => "{$repoUrl}/issues/1", 'content' => 'x']])[0]); |
| 592 | // 編集と削除は書いた人だけ |
| 593 | Federation::receive('owner001', 'fedi1', ['type' => 'Update', 'actor' => $alice, 'object' => $reply + ['content' => '<p>乗っ取り</p>']]); |
| 594 | $test->assertSame('<p>私もです</p>', $db->get(1)['comments'][0]['body']); |
| 595 | Federation::receive('owner001', 'fedi1', ['type' => 'Update', 'actor' => $bob, 'object' => ['content' => '<p>私もです(追記)</p>'] + $reply]); |
| 596 | $test->assertSame('<p>私もです(追記)</p>', $db->get(1)['comments'][0]['body']); |
| 597 | Federation::receive('owner001', 'fedi1', ['type' => 'Delete', 'actor' => $bob, 'object' => ['id' => $reply['id'], 'type' => 'Tombstone']]); |
| 598 | $test->assertSame(1, count($db->get(1)['comments'])); |
| 599 | $queue(); |
| 600 | }); |
| 601 | |
| 602 | $test->it('ローカルのコメントと閉じた事を送り、作った人のResolveで閉じるはず', function($test) use ($alice, $bob, $repoUrl, $queue): void { |
| 603 | $db = Issues::open('owner001', 'fedi1'); |
| 604 | $meta = Gitler::meta('owner001', 'fedi1'); |
| 605 | $res = $db->comment(1, ['user' => 'owner001'], "直します #1"); |
| 606 | Federation::commentAdded('owner001', 'fedi1', $meta, $res->data['issue'], $res->data['comment']); |
| 607 | $q = $queue(); |
| 608 | $test->assertSame('Create', $q[0][2]); |
| 609 | $obj = $q[0][3]['object']; |
| 610 | $test->assertSame("{$repoUrl}/issues/1/comments/".$res->data['comment']['id'], $obj['id']); |
| 611 | $test->assertSame("{$repoUrl}/issues/1", $obj['inReplyTo']); |
| 612 | $test->assertTrue(str_contains($obj['content'], 'href="https://lb.test/gitler/owner001/fedi1/issues/1"'), '絶対URL'); |
| 613 | $inboxes = array_unique(array_column($q, 1)); |
| 614 | sort($inboxes); |
| 615 | $test->assertSame(['https://8.8.8.8/inbox'], $inboxes, 'フォロワーと関わった人(共有の受付ボックス)'); |
| 616 | // Resolveは作った人だけ |
| 617 | Federation::receive('owner001', 'fedi1', ['type' => 'Resolve', 'actor' => $bob, 'object' => "{$repoUrl}/issues/1/ticket"]); |
| 618 | $test->assertSame('open', $db->get(1)['state']); |
| 619 | Federation::receive('owner001', 'fedi1', ['type' => 'Resolve', 'actor' => $alice, 'object' => "{$repoUrl}/issues/1/ticket"]); |
| 620 | $test->assertSame('closed', $db->get(1)['state']); |
| 621 | $types = array_column($queue(), 2); |
| 622 | $test->assertTrue(in_array('Resolve', $types, true) && in_array('Update', $types, true)); |
| 623 | Federation::receive('owner001', 'fedi1', ['type' => 'Undo', 'actor' => $alice, 'object' => ['type' => 'Resolve', 'actor' => $alice, 'object' => "{$repoUrl}/issues/1/ticket"]]); |
| 624 | $test->assertSame('open', $db->get(1)['state']); |
| 625 | $test->assertTrue(in_array('Undo', array_column($queue(), 2), true)); |
| 626 | }); |
| 627 | |
| 628 | $test->it('ForgeFedのOffer{Ticket}でイシューを作ってAcceptを返すはず', function($test) use ($bob, $repoUrl, $queue): void { |
| 629 | $offer = ['id' => 'https://9.9.9.9/offers/1', 'type' => 'Offer', 'actor' => $bob, 'target' => $repoUrl, |
| 630 | 'object' => ['type' => 'Ticket', 'attributedTo' => $bob, 'summary' => 'Tickets & more', 'content' => '<p>x</p>', |
| 631 | 'source' => ['mediaType' => 'text/markdown', 'content' => "**太字**\n"]]]; |
| 632 | Federation::receive('owner001', 'fedi1', $offer); |
| 633 | $i = Issues::open('owner001', 'fedi1')->get(2); |
| 634 | $test->assertSame('Tickets & more', $i['title']); |
| 635 | $test->assertSame("**太字**\n", $i['body']); |
| 636 | $test->assertFalse($i['html'], 'Markdownのまま'); |
| 637 | $accept = array_values(array_filter($queue(), fn($x) => $x[2] === 'Accept')); |
| 638 | $test->assertSame("{$repoUrl}/issues/2/ticket", $accept[0][3]['result']); |
| 639 | $t = Federation::ticket('owner001', 'fedi1', $i); |
| 640 | $test->assertSame('Ticket', $t['type']); |
| 641 | $test->assertSame($bob, $t['attributedTo']); |
| 642 | $test->assertFalse($t['isResolved']); |
| 643 | }); |
| 644 | |
| 645 | $test->it('ブロックした人は無視し、SSHのプッシュでは配信を溜めるはず', function($test) use ($bob, $repoUrl, $queue): void { |
| 646 | $test->assertTrue(Federation::block('owner001', 'fedi1', $bob)->isSuccess); |
| 647 | $test->assertTrue(Federation::block('owner001', 'fedi1', 'spam.example')->isSuccess); |
| 648 | $test->assertTrue(Federation::isBlocked('owner001', 'fedi1', 'https://a.spam.example/u/x')); |
| 649 | $before = count(Issues::open('owner001', 'fedi1')->get(1)['comments']); |
| 650 | Federation::receive('owner001', 'fedi1', ['type' => 'Create', 'actor' => $bob, 'object' => ['id' => 'https://9.9.9.9/notes/20', 'type' => 'Note', 'attributedTo' => $bob, |
| 651 | 'inReplyTo' => "{$repoUrl}/issues/1", 'content' => '<p>x</p>']]); |
| 652 | $test->assertSame($before, count(Issues::open('owner001', 'fedi1')->get(1)['comments'])); |
| 653 | Federation::block('owner001', 'fedi1', $bob, false); |
| 654 | $test->assertFalse(Federation::isBlocked('owner001', 'fedi1', $bob)); |
| 655 | $queue(); |
| 656 | Gitler::$deferred = true; |
| 657 | $issue = Issues::open('owner001', 'fedi1')->get(1); |
| 658 | Federation::stateChanged('owner001', 'fedi1', Gitler::meta('owner001', 'fedi1'), $issue); |
| 659 | Gitler::$deferred = false; |
| 660 | $test->assertSame([], $queue()); |
| 661 | $test->assertSame(2, count(glob(Gitler::path('owner001', 'fedi1').'/gitler/spool/*.json'))); |
| 662 | $test->assertSame(2, Gitler::flushDeferred()); |
| 663 | $test->assertSame([], glob(Gitler::path('owner001', 'fedi1').'/gitler/spool/*.json')); |
| 664 | $test->assertTrue(count($queue()) > 0); |
| 665 | }); |
| 666 | Fedi::$dataDir = null; |
| 667 | Fedi::$baseUrl = null; |
| 668 | }); |
| 669 | |
| 670 | $test->describe('Gitler(通知とアーカイブ)', function($test) use ($base, $owner, $other, $plain, $admin): void { |
| 671 | Notifications::$dataDir = "{$base}/data"; |
| 672 | Gitler::create($owner, 'notify1'); |
| 673 | $meta = Gitler::meta('owner001', 'notify1'); |
| 674 | $meta['collaborators'] = ['other001' => 'write']; |
| 675 | Gitler::saveMeta('owner001', 'notify1', $meta); |
| 676 | $meta = Gitler::meta('owner001', 'notify1'); |
| 677 | $db = Issues::open('owner001', 'notify1'); |
| 678 | $labels = fn(string $u) => array_map(fn($n) => $n['label'], Notifications::list($u)); |
| 679 | $clear = function(): void { |
| 680 | foreach (glob(Notifications::$dataDir.'/notifications/*.json') ?: [] as $f) unlink($f); |
| 681 | }; |
| 682 | |
| 683 | $test->it('「@ユーザー」を見つけるはず(コード、リモート、リポジトリは除く)', function($test): void { |
| 684 | $test->assertSame(['alice', 'bob'], Notify::mentions("@alice さん、@bob。\n`@code` @x@example.com @owner.repo mail@example.com\n```\n@block\n```\n")); |
| 685 | }); |
| 686 | |
| 687 | $test->it('所有者と共同作業者はウォッチし、作った人は自分の通知を受け取らないはず', function($test) use ($meta, $db, $labels, $clear): void { |
| 688 | $clear(); |
| 689 | $test->assertSame(Notify::WATCHING, Notify::watchState('owner001', 'notify1', 'owner001')); |
| 690 | $test->assertSame(Notify::PARTICIPATING, Notify::watchState('owner001', 'notify1', 'plain001')); |
| 691 | $test->assertSame(['owner001', 'other001'], Notify::watchers('owner001', 'notify1', $meta)); |
| 692 | $issue = $db->create(['user' => 'plain001'], '動かない', '@admin001 見て下さい')->data['issue']; |
| 693 | Notify::issueOpened('owner001', 'notify1', $meta, $db, $issue); |
| 694 | $test->assertSame(['がowner001/notify1にイシュー#1「動かない」を作りました'], $labels('owner001')); |
| 695 | $test->assertSame(1, count($labels('other001'))); |
| 696 | $test->assertSame([], $labels('plain001'), '自分の操作'); |
| 697 | $test->assertSame(['がowner001/notify1#1「動かない」であなたをメンションしました'], $labels('admin001')); |
| 698 | $test->assertSame(true, $db->get(1)['subscribers']['admin001'], 'メンションされた人は受け取る'); |
| 699 | $n = Notifications::list('owner001')[0]; |
| 700 | $test->assertSame(['username' => 'plain001'], $n['from']); |
| 701 | $test->assertSame('/gitler/owner001/notify1/issues/1', $n['page']); |
| 702 | $test->assertSame('@admin001 見て下さい', $n['excerpt']); |
| 703 | $test->assertSame(1, Notifications::unreadCount('owner001')); |
| 704 | Notifications::markRead('owner001'); |
| 705 | $test->assertSame(0, Notifications::unreadCount('owner001')); |
| 706 | }); |
| 707 | |
| 708 | $test->it('コメントは受け取る人に届き、止めた人と無視した人には届かないはず', function($test) use ($meta, $db, $labels, $clear): void { |
| 709 | $clear(); |
| 710 | Notify::setWatch('owner001', 'notify1', 'other001', Notify::IGNORING); |
| 711 | $db->subscribe(1, ['admin001'], false); |
| 712 | $res = $db->comment(1, ['user' => 'owner001'], '直します'); |
| 713 | Notify::commented('owner001', 'notify1', $meta, $db, $res->data['issue'], $res->data['comment']); |
| 714 | $test->assertSame(['がowner001/notify1#1「動かない」にコメントしました'], $labels('plain001')); |
| 715 | $test->assertSame('/gitler/owner001/notify1/issues/1#c1', Notifications::list('plain001')[0]['page']); |
| 716 | $test->assertSame([], $labels('other001'), '無視'); |
| 717 | $test->assertSame([], $labels('admin001'), '止めた'); |
| 718 | $test->assertSame([], $labels('owner001'), '自分'); |
| 719 | // コミットで閉じる |
| 720 | $clear(); |
| 721 | $issue = $db->setState(1, false, ['commit' => str_repeat('a', 40), 'name' => '諏訪子']); |
| 722 | Notify::stateChanged('owner001', 'notify1', $meta, $issue, ['commit' => str_repeat('a', 40), 'name' => '諏訪子']); |
| 723 | $test->assertSame(['のコミットaaaaaaaがowner001/notify1#1「動かない」を閉じました'], $labels('owner001')); |
| 724 | $test->assertSame(['name' => '諏訪子'], Notifications::list('owner001')[0]['from']); |
| 725 | $test->assertSame(1, count($labels('plain001'))); |
| 726 | // 担当者 |
| 727 | $clear(); |
| 728 | $new = $db->triage(1, ['user' => 'owner001'], null, ['other001'], false); |
| 729 | Notify::assigned('owner001', 'notify1', $meta, $new, ['other001'], ['user' => 'owner001']); |
| 730 | $test->assertSame([], $labels('other001'), '無視しているので担当も届かない'); |
| 731 | Notify::setWatch('owner001', 'notify1', 'other001', Notify::PARTICIPATING); |
| 732 | Notify::assigned('owner001', 'notify1', $meta, $new, ['other001'], ['user' => 'owner001']); |
| 733 | $test->assertSame(['がowner001/notify1#1「動かない」の担当者にあなたを追加しました'], $labels('other001')); |
| 734 | }); |
| 735 | |
| 736 | $test->it('SSHのプッシュでは通知を溜めて後で届けるはず', function($test) use ($meta, $db, $labels, $clear): void { |
| 737 | $clear(); |
| 738 | Gitler::$deferred = true; |
| 739 | $issue = $db->setState(1, true, ['user' => 'owner001']); |
| 740 | Notify::stateChanged('owner001', 'notify1', $meta, $issue, ['user' => 'owner001']); |
| 741 | Gitler::$deferred = false; |
| 742 | $test->assertSame([], $labels('plain001')); |
| 743 | $test->assertTrue(Gitler::flushDeferred() >= 1); |
| 744 | $test->assertSame(['がowner001/notify1#1「動かない」を開き直しました'], $labels('plain001')); |
| 745 | }); |
| 746 | |
| 747 | $test->it('メールの設定を保存し、アーカイブではイシューとFediverseのコメントを受け付けないはず', function($test) use ($meta, $db): void { |
| 748 | $test->assertTrue(Keys::load('plain001')['email']); |
| 749 | $test->assertTrue(Keys::setEmail('plain001', false)); |
| 750 | $test->assertFalse(Keys::load('plain001')['email']); |
| 751 | $test->assertSame([], Keys::load('plain001')['keys'], '鍵は消えない'); |
| 752 | $test->assertFalse($meta['archived']); |
| 753 | $meta['archived'] = true; |
| 754 | Gitler::saveMeta('owner001', 'notify1', $meta); |
| 755 | $test->assertTrue(Gitler::meta('owner001', 'notify1')['archived']); |
| 756 | if (Fedi::enabled()) { |
| 757 | $res = Federation::receive('owner001', 'notify1', ['type' => 'Create', 'actor' => 'https://8.8.8.8/users/alice', 'object' => ['id' => 'https://8.8.8.8/notes/x', 'type' => 'Note', |
| 758 | 'attributedTo' => 'https://8.8.8.8/users/alice', 'inReplyTo' => 'https://lb.test/gitler/owner001/notify1/issues/1', 'content' => '<p>x</p>']]); |
| 759 | $test->assertSame('アーカイブされています', $res[1]['status']); |
| 760 | } |
| 761 | $test->assertSame(1, count($db->get(1)['comments'])); |
| 762 | }); |
| 763 | Notifications::$dataDir = null; |
| 764 | }); |
| 765 | |
| 766 | $test->describe('Gitler(プルリクエストとフォーク)', function($test) use ($base, $admin, $owner, $mkUser): void { |
| 767 | Notifications::$dataDir = "{$base}/data"; |
| 768 | $forker = $mkUser(7, 'forker01', Roles::MEMBER | Roles::GIT_REPOS | Roles::GIT_PULLS); |
| 769 | $reader = $mkUser(8, 'reader01', Roles::MEMBER); |
| 770 | Gitler::create($admin, 'base1'); |
| 771 | $repo = Gitler::open('admin001', 'base1'); |
| 772 | $me = new Person('諏訪子', 's@x', 1700000000, '+0900'); |
| 773 | $commit = function(Repository $r, string $branch, array $files, string $msg) use ($me): string { |
| 774 | $parent = $r->refs->resolve('refs/heads/'.$branch); |
| 775 | $c = $r->writeCommit(Commit::create($r->updateTree($parent ? $r->commit($parent)->tree : null, $files), $parent ? [$parent] : [], $me, $me, $msg."\n")); |
| 776 | $r->refs->update('refs/heads/'.$branch, $c); |
| 777 | return $c; |
| 778 | }; |
| 779 | $c0 = $commit($repo, 'master', ['a.txt' => [Tree::FILE, "1\n2\n3\n4\n5\n"]], '最初'); |
| 780 | $issues = new Issues($repo); |
| 781 | $bug = $issues->create(['user' => 'admin001'], 'バグ', '')->data['number']; |
| 782 | |
| 783 | $test->it('同じリポジトリのブランチからプルリクエストを作ってマージするはず', function($test) use ($repo, $commit, $admin, $issues, $bug): void { |
| 784 | $repo->refs->update('refs/heads/topic', $repo->resolve('master')); |
| 785 | $head = $commit($repo, 'topic', ['b.txt' => [Tree::FILE, "b\n"]], 'bを追加'); |
| 786 | $test->assertFalse(Pulls::open('admin001', 'base1', ['user' => 'admin001'], 'x', '', 'master', 'branch', ['owner' => 'admin001', 'name' => 'base1', 'branch' => 'master'], $repo->resolve('master'))->isSuccess, '差分が無い'); |
| 787 | $res = Pulls::open('admin001', 'base1', ['user' => 'admin001'], 'bを追加', "fixes #{$bug}", 'master', 'branch', ['owner' => 'admin001', 'name' => 'base1', 'branch' => 'topic'], $head); |
| 788 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 789 | $n = $res->data['number']; |
| 790 | $test->assertSame($bug + 1, $n, 'イシューと同じ番号'); |
| 791 | $test->assertSame($head, $repo->refs->resolve(Pulls::ref($n))); |
| 792 | $test->assertSame([$n], array_column($issues->search(['pull' => true]), 'number')); |
| 793 | $test->assertSame([$bug], array_column($issues->search(), 'number'), 'イシューの一覧には出ない'); |
| 794 | $test->assertSame(['open' => 1, 'closed' => 0], $issues->counts(true)); |
| 795 | $test->assertFalse(Pulls::open('admin001', 'base1', ['user' => 'admin001'], 'y', '', 'master', 'branch', ['owner' => 'admin001', 'name' => 'base1', 'branch' => 'topic'], $head)->isSuccess, '同じ物'); |
| 796 | // ブランチにプッシュすると更新 |
| 797 | $head2 = $commit($repo, 'topic', ['c.txt' => [Tree::FILE, "c\n"]], 'cを追加'); |
| 798 | Pulls::afterPush('admin001', 'base1', $admin, [['refs/heads/topic', $head, $head2]]); |
| 799 | $i = $issues->get($n); |
| 800 | $test->assertSame($head2, $i['pull']['headId']); |
| 801 | $test->assertSame(1, end($i['events'])['count']); |
| 802 | $test->assertSame('ff', Pulls::mergeability($repo, $i)['state']); |
| 803 | $old = $repo->resolve('master'); |
| 804 | $res = Pulls::merge('admin001', 'base1', $n, $admin, 'merge', ''); |
| 805 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 806 | $m = $repo->commit($repo->resolve('master')); |
| 807 | $test->assertSame([$old, $head2], $m->parents); |
| 808 | $test->assertSame("Merge pull request #{$n}", $m->subject()); |
| 809 | $i = $issues->get($n); |
| 810 | $test->assertSame('closed', $i['state']); |
| 811 | $test->assertSame('merge', $i['pull']['merged']['method']); |
| 812 | $test->assertSame('closed', $issues->get($bug)['state'], '本文の「fixes」'); |
| 813 | $test->assertSame([$old, $head2], Pulls::range($repo, $i)); |
| 814 | $test->assertFalse(Pulls::merge('admin001', 'base1', $n, $admin, 'merge', '')->isSuccess, '2回目'); |
| 815 | }); |
| 816 | |
| 817 | $test->it('コンフリクトを見つけ、スカッシュとリベースでマージするはず', function($test) use ($repo, $commit, $admin, $issues): void { |
| 818 | $mk = function(string $branch, array $files) use ($repo, $commit): string { |
| 819 | $repo->refs->update('refs/heads/'.$branch, $repo->resolve('master')); |
| 820 | return $commit($repo, $branch, $files, $branch); |
| 821 | }; |
| 822 | $open = fn(string $branch, string $head) => Pulls::open('admin001', 'base1', ['user' => 'admin001'], $branch, '', 'master', 'branch', ['owner' => 'admin001', 'name' => 'base1', 'branch' => $branch], $head)->data['number']; |
| 823 | $h1 = $mk('one', ['a.txt' => [Tree::FILE, "1\nONE\n3\n4\n5\n"]]); |
| 824 | $h2 = $mk('two', ['a.txt' => [Tree::FILE, "1\nTWO\n3\n4\n5\n"]]); |
| 825 | $h3 = $commit($repo, 'one', ['d.txt' => [Tree::FILE, "d\n"]], 'one-2'); |
| 826 | $h4 = $mk('three', ['a.txt' => [Tree::FILE, "1\n2\n3\n4\nTHREE\n"]]); |
| 827 | $n1 = $open('one', $h3); |
| 828 | $n2 = $open('two', $h2); |
| 829 | $n4 = $open('three', $h4); |
| 830 | $base = $repo->resolve('master'); |
| 831 | $test->assertTrue(Pulls::merge('admin001', 'base1', $n1, $admin, 'squash', '')->isSuccess); |
| 832 | $sq = $repo->commit($repo->resolve('master')); |
| 833 | $test->assertSame([$base], $sq->parents, 'スカッシュは1つのコミット'); |
| 834 | $test->assertSame("one (#{$n1})", $sq->subject()); |
| 835 | $test->assertSame('諏訪子', $repo->commit($h3)->author->name); |
| 836 | $test->assertSame('conflict', Pulls::mergeability($repo, $issues->get($n2))['state']); |
| 837 | $test->assertSame(['a.txt'], Pulls::mergeability($repo, $issues->get($n2))['conflicts']); |
| 838 | $test->assertFalse(Pulls::merge('admin001', 'base1', $n2, $admin, 'merge', '')->isSuccess); |
| 839 | $test->assertFalse(Pulls::merge('admin001', 'base1', $n2, $admin, 'rebase', '')->isSuccess); |
| 840 | $test->assertSame('clean', Pulls::mergeability($repo, $issues->get($n4))['state']); |
| 841 | $before = $repo->resolve('master'); |
| 842 | $test->assertTrue(Pulls::merge('admin001', 'base1', $n4, $admin, 'rebase', '')->isSuccess); |
| 843 | $rb = $repo->commit($repo->resolve('master')); |
| 844 | $test->assertSame([$before], $rb->parents); |
| 845 | $test->assertSame('three', $rb->subject()); |
| 846 | $test->assertSame("1\nONE\n3\n4\nTHREE\n", $repo->blob($repo->resolve('master:a.txt'))); |
| 847 | }); |
| 848 | |
| 849 | $test->it('フォークしてプルリクエストを作り、フォークへのプッシュで更新するはず', function($test) use ($repo, $commit, $forker, $reader, $issues): void { |
| 850 | $test->assertFalse(Gitler::fork($reader, 'admin001', 'base1')->isSuccess, 'ロールが無い'); |
| 851 | $res = Gitler::fork($forker, 'admin001', 'base1'); |
| 852 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 853 | $test->assertSame('base1', $res->data['name']); |
| 854 | $test->assertSame('admin001/base1', Gitler::meta('forker01', 'base1')['forkOf']); |
| 855 | $test->assertSame([['forker01', 'base1']], Gitler::forks('admin001', 'base1')); |
| 856 | $test->assertSame('base1', Gitler::fork($forker, 'admin001', 'base1')->data['name'], '2回目は同じフォーク'); |
| 857 | $fork = Gitler::open('forker01', 'base1'); |
| 858 | $test->assertSame($repo->resolve('master'), $fork->resolve('master')); |
| 859 | $test->assertSame([], $fork->refs->all('refs/pull/')); |
| 860 | $f1 = $commit($fork, 'feature', ['f.txt' => [Tree::FILE, "f\n"]], 'フォークで追加'); |
| 861 | $test->assertFalse($repo->objects->exists($f1)); |
| 862 | Pulls::fetch($fork, $repo, $f1); |
| 863 | $test->assertTrue($repo->objects->exists($f1)); |
| 864 | $res = Pulls::open('admin001', 'base1', ['user' => 'forker01'], 'フォークから', '', 'master', 'fork', ['owner' => 'forker01', 'name' => 'base1', 'branch' => 'feature'], $f1); |
| 865 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 866 | $n = $res->data['number']; |
| 867 | $f2 = $commit($fork, 'feature', ['f.txt' => [Tree::FILE, "f2\n"]], 'もう1つ'); |
| 868 | $msgs = Pulls::afterPush('forker01', 'base1', $forker, [['refs/heads/feature', $f1, $f2]]); |
| 869 | $test->assertSame($f2, $issues->get($n)['pull']['headId']); |
| 870 | $test->assertTrue($repo->objects->exists($f2)); |
| 871 | $test->assertSame([], $msgs); |
| 872 | $msgs = Pulls::afterPush('forker01', 'base1', $forker, [['refs/heads/new', $fork->zeroId(), $f2]]); |
| 873 | $test->assertStringContains('/gitler/admin001/base1/compare?head=forker01%2Fbase1%3Anew', $msgs[0] ?? ''); |
| 874 | }); |
| 875 | |
| 876 | $test->it('refs/for/へのプッシュ(AGit)でプルリクエストを作って更新するはず', function($test) use ($repo, $forker, $reader, $issues, $me): void { |
| 877 | $zero = $repo->zeroId(); |
| 878 | $test->assertSame('pull requests are not allowed', Pulls::checkAgit('admin001', 'base1', $reader, 'refs/for/master/x', str_repeat('1', 40), $zero)); |
| 879 | $test->assertSame('use refs/for/<branch>/<topic>', Pulls::checkAgit('admin001', 'base1', $forker, 'refs/for/nope/x', str_repeat('1', 40), $zero)); |
| 880 | $test->assertNull(Pulls::checkAgit('admin001', 'base1', $forker, 'refs/for/master/fix/typo', str_repeat('1', 40), $zero)); |
| 881 | $test->assertSame(['master', 'fix/typo'], Pulls::parseAgit($repo, 'refs/for/master/fix/typo')); |
| 882 | $master = $repo->resolve('master'); |
| 883 | $a1 = $repo->writeCommit(Commit::create($repo->updateTree($repo->commit($master)->tree, ['typo.txt' => [Tree::FILE, "x\n"]]), [$master], $me, $me, "誤字を直す\n\n説明\n")); |
| 884 | $msgs = Pulls::afterPush('admin001', 'base1', $forker, [['refs/for/master/fix/typo', $zero, $a1]]); |
| 885 | $test->assertStringContains('を作りました', $msgs[0]); |
| 886 | $n = (int)preg_replace('/^.*#(\d+).*$/u', '$1', $msgs[0]); |
| 887 | $i = $issues->get($n); |
| 888 | $test->assertSame('誤字を直す', $i['title']); |
| 889 | $test->assertSame('説明', $i['body']); |
| 890 | $test->assertSame(['user' => 'forker01', 'topic' => 'fix/typo'], $i['pull']['head']); |
| 891 | $a2 = $repo->writeCommit(Commit::create($repo->commit($a1)->tree, [$a1], $me, $me, "2\n")); |
| 892 | $msgs = Pulls::afterPush('admin001', 'base1', $forker, [['refs/for/master/fix/typo', $zero, $a2]]); |
| 893 | $test->assertStringContains("#{$n}を更新しました", $msgs[0]); |
| 894 | $test->assertSame($a2, $repo->refs->resolve(Pulls::ref($n))); |
| 895 | // マージ先に入ったらマージ済み |
| 896 | $old = $repo->resolve('master'); |
| 897 | $repo->refs->update('refs/heads/master', $a2); |
| 898 | Pulls::afterPush('admin001', 'base1', null, [['refs/heads/master', $old, $a2]]); |
| 899 | $test->assertSame('manual', $issues->get($n)['pull']['merged']['method']); |
| 900 | }); |
| 901 | |
| 902 | $test->it('行へのコメントの下書きをレビューに入れるはず', function($test) use ($issues): void { |
| 903 | $n = $issues->search(['pull' => true, 'state' => 'all'])[0]['number']; |
| 904 | $at = ['path' => 'a.txt', 'side' => 'new', 'line' => 2, 'commit' => 'x', 'hunk' => [' 1', '+2'], 'pending' => true]; |
| 905 | $c = $issues->inlineComment($n, ['user' => 'reader01'], 'ここ', $at)->data['comment']; |
| 906 | $test->assertTrue($c['pending']); |
| 907 | $before = $issues->index()['issues'][$n]['comments']; |
| 908 | $test->assertFalse($issues->submitReview($n, ['user' => 'other001'], 'commented', '', 'x')->isSuccess, '空のレビュー'); |
| 909 | $r = $issues->submitReview($n, ['user' => 'reader01'], 'approved', 'いいね', 'x'); |
| 910 | $test->assertTrue($r->isSuccess); |
| 911 | $i = $issues->get($n); |
| 912 | $test->assertSame(1, $i['reviews'][0]['id']); |
| 913 | $test->assertFalse(end($i['comments'])['pending']); |
| 914 | $test->assertSame(1, end($i['comments'])['review']); |
| 915 | $test->assertSame($before + 1, $issues->index()['issues'][$n]['comments']); |
| 916 | $reply = $issues->inlineComment($n, ['user' => 'admin001'], '直します', ['replyTo' => $c['id']])->data['comment']; |
| 917 | $test->assertSame(['a.txt', 2, $c['id']], [$reply['path'], $reply['line'], $reply['replyTo']]); |
| 918 | }); |
| 919 | Notifications::$dataDir = null; |
| 920 | }); |
| 921 | |
| 922 | $test->describe('Gitler(HTTPのパスワード)', function($test) use ($base): void { |
| 923 | if (!AUTH_ENABLED) { |
| 924 | $test->skipAll('AUTH_ENABLEDが無効'); |
| 925 | return; |
| 926 | } |
| 927 | $_SERVER['REMOTE_ADDR'] ??= '127.0.0.1'; |
| 928 | $test->it('パスワードだけを確かめ、二段階認証のユーザーは使えないはず', function($test) use ($base): void { |
| 929 | $pass = 'testTEST1234!#$%@[]:;'; |
| 930 | $auth = new class($base) extends Auth { |
| 931 | public function __construct(string $base) { |
| 932 | $this->dataDir = "{$base}/user/"; |
| 933 | $this->assDir = "{$base}/ass/"; |
| 934 | parent::__construct(); |
| 935 | } |
| 936 | public function twoFactorAvailable(): bool { return true; } |
| 937 | }; |
| 938 | $u = ['id' => 10, 'username' => 'pass0001', 'email' => 'p@x', 'role' => Roles::MEMBER, 'password' => password_hash($pass, PASSWORD_DEFAULT), 'tokens' => []]; |
| 939 | file_put_contents("{$base}/user/10.pass0001.json", json_encode($u)); |
| 940 | $ok = $auth->checkPassword('pass0001', $pass); |
| 941 | $test->assertSame('pass0001', $ok?->username); |
| 942 | $test->assertFalse(isset($ok->password)); |
| 943 | $test->assertNull($auth->checkPassword('pass0001', 'machigatta')); |
| 944 | $test->assertNull($auth->checkPassword('nobody01', $pass)); |
| 945 | $u['twoFactor'] = ['secret' => 'ABC']; |
| 946 | file_put_contents("{$base}/user/10.pass0001.json", json_encode($u)); |
| 947 | $test->assertNull($auth->checkPassword('pass0001', $pass), '二段階認証'); |
| 948 | $auth->clearLoginFailures(); |
| 949 | }); |
| 950 | }); |
| 951 | |
| 952 | $test->printSummary(); |