true, 'verboseOutput' => true ]); // 本物のユーザーデータを触らない様に、一時ディレクトリを使う $base = sys_get_temp_dir().'/lbauth_'.bin2hex(random_bytes(6)); mkdir("{$base}/user", 0700, true); mkdir("{$base}/ass", 0700, true); touch("{$base}/user/.kara"); // メールは送らずに記録する // メールアドレスのMXレコードを確認するので、DNSが使えない場合は登録のテストをスキップする $dns = checkdnsrr('076.co.jp', 'MX'); $mails = new \ArrayObject(); // $twoFactor:config.phpのAUTH_2FA_ENABLEDの代わり $make = function(?string $username = null, bool $verify = false, bool $mailFails = false, bool $twoFactor = true) use ($base, $mails): Auth { return new class($username, $base, $verify, $mailFails, $mails, $twoFactor) extends Auth { public function __construct(?string $username, string $base, private bool $verify, private bool $mailFails, private \ArrayObject $mails, private bool $twoFactor) { $this->dataDir = "{$base}/user/"; $this->assDir = "{$base}/ass/"; parent::__construct($username); } public function requiresVerification(): bool { return $this->verify; } public function twoFactorAvailable(): bool { return $this->twoFactor; } protected function siteUrl(): string { return 'https://example.com'; } protected function sendVerificationMail(\stdClass $user, string $token): Result { if ($this->mailFails) return Result::Error('送信失敗'); $this->mails[] = ['to' => $user->email, 'token' => $token]; return Result::Success(); } }; }; $userFile = function(string $username) use ($base): ?string { foreach (scandir("{$base}/user") as $f) if (preg_match('/^\d+\.'.preg_quote($username, '/').'\.json$/', $f)) return "{$base}/user/{$f}"; return null; }; $read = fn(string $username) => json_decode(file_get_contents($userFile($username))); $pass = 'testTEST1234!#$%@[]:;'; $test->describe('ユーザー登録', function($test) use ($make, $read, $userFile, $pass, $base, $dns): void { if (!AUTH_ENABLED || !AUTH_REGISTER_ENABLED) { $test->skipAll('AUTH_ENABLED又はAUTH_REGISTER_ENABLEDが無効'); } else if (!$dns) { $test->skipAll('DNSが使えない(076.co.jpのMXレコードを受け取れない)'); } $test->it('アカウントを作成出来るはず', function($test) use ($make, $read, $pass, $base): void { $res = $make()->mkUser('admin1234', $pass, $pass, 'test@076.co.jp'); $test->assertTrue($res->isSuccess, (string)$res->message); $test->assertFalse($res->data['verify']); $u = $read('admin1234'); $test->assertEquals(1, $u->id); $test->assertEquals('test@076.co.jp', $u->email); $test->assertTrue(password_verify($pass, $u->password)); $test->assertFalse(isset($u->verified)); $test->assertTrue(is_dir("{$base}/ass/admin1234")); }); $test->it('空フィールドでアカウントを作成出来ないはず', function($test) use ($make, $pass): void { $auth = $make(); $test->assertEquals('エラー:ユーザー名をご入力下さい。
', $auth->mkUser('', $pass, $pass, 'a@076.co.jp')->message); $test->assertEquals('エラー:パスワードをご入力下さい。
', $auth->mkUser('admin5678', '', $pass, 'a@076.co.jp')->message); $test->assertEquals('エラー:メールアドレスをご入力下さい。
', $auth->mkUser('admin5678', $pass, $pass, '')->message); }); $test->it('既にユーザー名やメールアドレスが存在するアカウントを作成出来ないはず', function($test) use ($make, $pass): void { $auth = $make(); $test->assertEquals('ユーザー「admin1234」は既に存在します。
', $auth->mkUser('admin1234', $pass, $pass, 'b@076.co.jp')->message); $test->assertEquals('ユーザー「test@076.co.jp」は既に存在します。
', $auth->mkUser('admin5678', $pass, $pass, 'test@076.co.jp')->message); }); $test->it('一致しない、短い、又は不安なパスワードでアカウントを作成出来ないはず', function($test) use ($make, $pass): void { $auth = $make(); $test->assertEquals('エラー:パスワードは一致していません。
', $auth->mkUser('admin5678', 'testtest1234!#$%@[]:;', $pass, 'c@076.co.jp')->message); $test->assertEquals('エラー:パスワードは20以上をご入力下さい。
', $auth->mkUser('admin5678', 'testTEST1234', 'testTEST1234', 'c@076.co.jp')->message); $test->assertEquals('エラー:パスワードは2つ以上の大文字、2つ以上の小文字、2つ以上の数字、及び2つ以上の特別文字を含む事が必須です。
', $auth->mkUser('admin5678', 'testTEST1234testTEST', 'testTEST1234testTEST', 'c@076.co.jp')->message); }); $test->it('不正なユーザー名とメールアドレスでアカウントを作成出来ないはず', function($test) use ($make, $pass): void { $auth = $make(); $test->assertEquals('エラー:ユーザー名は6文字以上をご入力下さい。
', $auth->mkUser('abc', $pass, $pass, 'c@076.co.jp')->message); $test->assertEquals('エラー:ユーザー名に不正な文字が含みます。
', $auth->mkUser('admin_../x', $pass, $pass, 'c@076.co.jp')->message); $test->assertEquals('エラー:メールアドレスは不正です。
', $auth->mkUser('admin5678', $pass, $pass, 'test@076')->message); $test->assertEquals('エラー:メールアドレスは不正です。
', $auth->mkUser('admin5678', $pass, $pass, 'test@nomx.invalid')->message); }); $test->it('公開プロフィールに秘密の情報を含まないはず', function($test) use ($make): void { $u = $make()->getUser('admin1234'); $test->assertNotNull($u); $test->assertFalse(isset($u->password)); $test->assertFalse(isset($u->tokens)); $test->assertFalse(isset($u->verifyToken)); $test->assertEquals('(秘密)', $u->email); $test->assertNull($make()->getUser('nobody99')); }); }); $test->describe('メールアドレスの確認', function($test) use ($make, $read, $userFile, $pass, $mails, $base, $dns): void { if (!AUTH_ENABLED || !AUTH_REGISTER_ENABLED) { $test->skipAll('AUTH_ENABLED又はAUTH_REGISTER_ENABLEDが無効'); } else if (!$dns) { $test->skipAll('DNSが使えない(076.co.jpのMXレコードを受け取れない)'); } $test->it('登録すると確認メールを送り、未確認として保存するはず', function($test) use ($make, $read, $pass, $mails): void { $res = $make(null, true)->mkUser('verify01', $pass, $pass, 'v1@076.co.jp'); $test->assertTrue($res->isSuccess); $test->assertTrue($res->data['verify']); $test->assertStringContains('確認メール', $res->message); $test->assertCount(1, $mails); $test->assertEquals('v1@076.co.jp', $mails[0]['to']); $test->assertTrue((bool)preg_match('/^[0-9a-f]{64}$/', $mails[0]['token'])); $u = $read('verify01'); $test->assertFalse($u->verified); // トークンはハッシュ化して保存する $test->assertEquals(hash('sha256', $mails[0]['token']), $u->verifyToken); $test->assertTrue($u->verifyExpire > time()); }); $test->it('確認メールの送信に失敗したら登録を取り消すはず', function($test) use ($make, $userFile, $pass, $base): void { $res = $make(null, true, true)->mkUser('verify02', $pass, $pass, 'v2@076.co.jp'); $test->assertFalse($res->isSuccess); $test->assertNull($userFile('verify02')); $test->assertFalse(is_dir("{$base}/ass/verify02")); }); $test->it('未確認のアカウントではログイン出来ないはず', function($test) use ($make, $pass, $mails): void { $res = $make('verify01', true)->setToken('verify01', $pass); $test->assertFalse($res->isSuccess); $test->assertTrue($res->data['unverified'] ?? false); // 10分以内は再送しない $test->assertCount(1, $mails); $test->assertFalse(str_contains($res->message, '再送')); }); $test->it('間違ったパスワードでは未確認かどうか教えないはず', function($test) use ($make): void { $res = $make('verify01', true)->setToken('verify01', 'machigatta-password'); $test->assertFalse($res->isSuccess); $test->assertFalse(isset($res->data['unverified'])); }); $test->it('時間が経てばログインの時に確認メールを再送するはず', function($test) use ($make, $read, $userFile, $pass, $mails): void { $path = $userFile('verify01'); $u = $read('verify01'); $u->verifySent = time() - 3600; file_put_contents($path, json_encode($u)); $res = $make('verify01', true)->setToken('verify01', $pass); $test->assertFalse($res->isSuccess); $test->assertStringContains('再送', $res->message); $test->assertCount(2, $mails); $test->assertFalse($mails[0]['token'] === $mails[1]['token']); $test->assertEquals(hash('sha256', $mails[1]['token']), $read('verify01')->verifyToken); }); $test->it('不正・古いトークンでは確認出来ないはず', function($test) use ($make, $read, $userFile, $mails): void { $auth = $make(); $test->assertFalse($auth->verifyEmailToken('')->isSuccess); $test->assertFalse($auth->verifyEmailToken('../../etc/passwd')->isSuccess); $test->assertFalse($auth->verifyEmailToken(str_repeat('0', 64))->isSuccess); // 再送したので、最初のトークンはもう使えない $test->assertFalse($auth->verifyEmailToken($mails[0]['token'])->isSuccess); // 有効期限切れ $path = $userFile('verify01'); $u = $read('verify01'); $expire = $u->verifyExpire; $u->verifyExpire = time() - 1; file_put_contents($path, json_encode($u)); $test->assertFalse($auth->verifyEmailToken($mails[1]['token'])->isSuccess); $u->verifyExpire = $expire; file_put_contents($path, json_encode($u)); }); $test->it('リンクのトークンで確認するとログイン出来るはず', function($test) use ($make, $read, $pass, $mails): void { $res = $make()->verifyEmailToken($mails[1]['token']); $test->assertTrue($res->isSuccess); $test->assertEquals('verify01', $res->data['username']); $u = $read('verify01'); $test->assertTrue($u->verified); $test->assertFalse(isset($u->verifyToken)); $test->assertFalse(isset($u->verifyExpire)); // 2回目は使えない $test->assertFalse($make()->verifyEmailToken($mails[1]['token'])->isSuccess); $_SERVER['REMOTE_ADDR'] ??= '127.0.0.1'; $res = @$make('verify01', true)->setToken('verify01', $pass); // CLIでは出力の後なのでクッキーを設定出来ないが、トークンは保存される $test->assertTrue($res->isSuccess || $res->message === 'エラー:クッキーを設定に失敗。', (string)$res->message); $test->assertCount(1, $read('verify01')->tokens); }); $test->it('確認機能の前に作ったアカウントは確認済みとして扱うはず', function($test) use ($make, $pass): void { $res = @$make('admin1234', true)->setToken('admin1234', $pass); $test->assertFalse(isset($res->data['unverified'])); $test->assertTrue($res->isSuccess || $res->message === 'エラー:クッキーを設定に失敗。', (string)$res->message); }); $test->it('古い未確認のアカウントを削除するはず', function($test) use ($make, $read, $userFile, $pass, $base): void { $make(null, true)->mkUser('verify03', $pass, $pass, 'v3@076.co.jp'); $make(null, true)->mkUser('verify04', $pass, $pass, 'v4@076.co.jp'); $path = $userFile('verify03'); $u = $read('verify03'); $u->regDate = time() - 8 * 86400; file_put_contents($path, json_encode($u)); $res = $make()->purgeUnverified(); $test->assertTrue($res->isSuccess); $test->assertEquals(['verify03'], $res->data['deleted']); $test->assertNull($userFile('verify03')); $test->assertFalse(is_dir("{$base}/ass/verify03")); // 新しい未確認・確認済み・確認機能の前のアカウントは残る $test->assertNotNull($userFile('verify04')); $test->assertNotNull($userFile('verify01')); $test->assertNotNull($userFile('admin1234')); // 削除したので同じ名前で登録出来る $test->assertTrue($make(null, true)->mkUser('verify03', $pass, $pass, 'v3@076.co.jp')->isSuccess); }); }); $test->describe('二段階認証', function($test) use ($make, $read, $userFile, $pass, $dns): void { if (!AUTH_ENABLED || !AUTH_REGISTER_ENABLED) { $test->skipAll('AUTH_ENABLED又はAUTH_REGISTER_ENABLEDが無効'); } else if (!$dns) { $test->skipAll('DNSが使えない(076.co.jpのMXレコードを受け取れない)'); } $_SERVER['REMOTE_ADDR'] ??= '127.0.0.1'; // CLIでは出力の後なのでクッキーを設定出来ないが、トークンは保存される $loggedIn = fn(Result $r) => $r->isSuccess || $r->message === 'エラー:クッキーを設定に失敗。'; $test->it('config.phpで無効なら誰も使えないはず', function($test) use ($make): void { $test->assertFalse($make('verify01', false, false, false)->twoFactorStatus('verify01')->isSuccess); $test->assertFalse($make('verify01', false, false, false)->enableTwoFactor('verify01', '123456')->isSuccess); }); $test->it('秘密鍵とQRコードのURIを出し、正しいコードで有効になるはず', function($test) use ($make, $read): void { $s = $make('verify01')->twoFactorStatus('verify01'); $test->assertTrue($s->isSuccess, (string)$s->message); $test->assertFalse($s->data['enabled']); $secret = $s->data['secret']; $test->assertEquals(32, strlen($secret)); $test->assertTrue(str_starts_with($s->data['uri'], 'otpauth://totp/')); $test->assertStringContains('secret='.$secret, $s->data['uri']); // 開き直しても同じ秘密鍵 $test->assertEquals($secret, $make('verify01')->twoFactorStatus('verify01')->data['secret']); $test->assertFalse($make('verify01')->enableTwoFactor('verify01', '000000')->isSuccess); $res = $make('verify01')->enableTwoFactor('verify01', Totp::code($secret)); $test->assertTrue($res->isSuccess, (string)$res->message); $u = $read('verify01'); $test->assertEquals($secret, $u->twoFactor->secret); $test->assertFalse(isset($u->twoFactorPending)); $test->assertTrue($make('verify01')->twoFactorStatus('verify01')->data['enabled']); $test->assertFalse(isset($make('verify01')->twoFactorStatus('verify01')->data['secret'])); }); $test->it('プロフィールに秘密鍵を含まないはず', function($test) use ($make): void { $test->assertFalse(isset($make('verify01')->getUser('verify01')->twoFactor)); $e = $make('verify01')->getEditableUser('verify01'); $test->assertTrue($e->twoFactorOn); $test->assertFalse(isset($e->twoFactor)); $test->assertFalse(isset($make('verify01')->getLoggedInUser()->twoFactor)); }); $test->it('パスワードの後にコードが必要で、同じコードは2度使えないはず', function($test) use ($make, $read, $pass, $loggedIn): void { $tokens = count($read('verify01')->tokens ?? []); $res = $make('verify01')->setToken('verify01', $pass); $test->assertFalse($res->isSuccess); $test->assertTrue($res->data['twoFactor'] ?? false); $ticket = $res->data['ticket']; $test->assertCount($tokens, $read('verify01')->tokens ?? []); // 間違ったパスワードではコードの画面にならない $test->assertFalse(isset($make('verify01')->setToken('verify01', 'machigatta-password')->data['twoFactor'])); $secret = $read('verify01')->twoFactor->secret; $bad = $make('verify01')->verifyTwoFactor('verify01', $ticket, '000000'); $test->assertFalse($bad->isSuccess); $test->assertEquals($ticket, $bad->data['ticket']); $test->assertFalse($make('verify01')->verifyTwoFactor('verify01', 'machigatta', Totp::code($secret, time() + 30))->isSuccess); $test->assertFalse($make('admin1234')->verifyTwoFactor('admin1234', $ticket, Totp::code($secret, time() + 30))->isSuccess); // 有効にした時のコードは使えないので、次の30秒のコード $code = Totp::code($secret, time() + 30); $ok = @$make('verify01')->verifyTwoFactor('verify01', $ticket, $code); $test->assertTrue($loggedIn($ok), (string)$ok->message); $test->assertCount($tokens + 1, $read('verify01')->tokens); $test->assertFalse(isset($read('verify01')->twoFactorTicket)); // チケットも同じコードも、もう使えない $test->assertFalse($make('verify01')->verifyTwoFactor('verify01', $ticket, $code)->isSuccess); $ticket2 = $make('verify01')->setToken('verify01', $pass)->data['ticket']; $test->assertFalse($make('verify01')->verifyTwoFactor('verify01', $ticket2, $code)->isSuccess); }); $test->it('5回間違えたらやり直しのはず', function($test) use ($make, $read, $pass): void { $ticket = $make('verify01')->setToken('verify01', $pass)->data['ticket']; for ($i = 0; $i < 4; $i++) $test->assertTrue(isset($make('verify01')->verifyTwoFactor('verify01', $ticket, '000000')->data['ticket'])); $test->assertFalse(isset($make('verify01')->verifyTwoFactor('verify01', $ticket, '000000')->data['ticket'])); $code = Totp::code($read('verify01')->twoFactor->secret, time() + 30); $test->assertFalse($make('verify01')->verifyTwoFactor('verify01', $ticket, $code)->isSuccess); }); $test->it('無効にするには本人はパスワードとコード、管理者は自分のパスワードが必要なはず', function($test) use ($make, $read, $userFile, $pass, $loggedIn): void { $admin = $read('admin1234'); $admin->role |= \Roles::ADMIN; file_put_contents($userFile('admin1234'), json_encode($admin)); $test->assertFalse($make('verify04')->twoFactorStatus('verify01')->isSuccess, '他のユーザーは見れない'); $test->assertFalse($make('verify01')->disableTwoFactor('verify01', $pass, '000000')->isSuccess); $test->assertFalse($make('verify01')->disableTwoFactor('verify01', 'machigatta-password', Totp::code($read('verify01')->twoFactor->secret, time() + 60))->isSuccess); $test->assertFalse($make('admin1234')->disableTwoFactor('verify01', 'machigatta-password')->isSuccess); $test->assertFalse($make('admin1234')->enableTwoFactor('verify01', '123456')->isSuccess, '管理者でも他のユーザーの分は有効に出来ない'); $res = $make('admin1234')->disableTwoFactor('verify01', $pass); $test->assertTrue($res->isSuccess, (string)$res->message); $test->assertFalse(isset($read('verify01')->twoFactor)); // 無効になったので、パスワードだけでログイン出来る $test->assertTrue($loggedIn(@$make('verify01')->setToken('verify01', $pass))); }); $test->it('config.phpで無効にすると、有効にしたユーザーもコード無しでログインするはず', function($test) use ($make, $read, $pass, $loggedIn): void { $secret = $make('verify01')->twoFactorStatus('verify01')->data['secret']; $test->assertTrue($make('verify01')->enableTwoFactor('verify01', Totp::code($secret))->isSuccess); $res = @$make('verify01', false, false, false)->setToken('verify01', $pass); $test->assertFalse(isset($res->data['twoFactor'])); $test->assertTrue($loggedIn($res)); }); }); $test->describe('表示の設定', function($test) use ($make, $read, $base): void { if (!AUTH_ENABLED) $test->skipAll('AUTH_ENABLEDが無効'); // 登録(DNSが必要)を使わずにユーザーを作る foreach (['prefuser01' => 901, 'prefuser02' => 902] as $name => $id) { file_put_contents("{$base}/user/{$id}.{$name}.json", json_encode(['id' => $id, 'username' => $name, 'role' => \Roles::MEMBER, 'tokens' => []])); } $test->it('ログイン中のユーザーのプロフィールに保存するはず', function($test) use ($make, $read): void { $res = $make('prefuser01')->savePrefs(['theme' => 'loli', 'glowring_off' => 1]); $test->assertTrue($res->isSuccess, (string)$res->message); $test->assertEquals('loli', $read('prefuser01')->prefs->theme); $test->assertEquals('1', $read('prefuser01')->prefs->glowring_off); $test->assertFalse(isset($read('prefuser02')->prefs), '他のユーザーは変わらない'); }); $test->it('他の設定を残したまま変更・削除出来るはず', function($test) use ($make, $read): void { $test->assertTrue($make('prefuser01')->savePrefs(['theme' => 'void'])->isSuccess); $test->assertEquals('void', $read('prefuser01')->prefs->theme); $test->assertEquals('1', $read('prefuser01')->prefs->glowring_off); $test->assertTrue($make('prefuser01')->savePrefs(['glowring_off' => null])->isSuccess); $test->assertFalse(isset($read('prefuser01')->prefs->glowring_off)); $test->assertEquals(901, $read('prefuser01')->id, 'ユーザーデータは消えない'); }); $test->it('ログインしていなければ保存しないはず', function($test) use ($make): void { $test->assertFalse($make()->savePrefs(['theme' => 'tux'])->isSuccess); }); }); $test->describe('管理画面', function($test) use ($make, $read, $userFile, $pass, $base): void { if (!AUTH_ENABLED) $test->skipAll('AUTH_ENABLEDが無効'); // data/user/が無い所から始める(新しいサイト) $empty = "{$base}/empty"; $makeIn = function(?string $username = null) use ($empty): Auth { return new class($username, $empty) extends Auth { public function __construct(?string $username, string $dir) { $this->dataDir = "{$dir}/user/"; $this->assDir = "{$dir}/ass/"; parent::__construct($username); } public function requiresVerification(): bool { return false; } }; }; $readIn = function(string $name) use ($empty): ?\stdClass { foreach (glob("{$empty}/user/*.{$name}.json") ?: [] as $f) return json_decode((string)file_get_contents($f)); return null; }; $_SERVER['REMOTE_ADDR'] ??= '127.0.0.1'; $test->it('管理者が居なければ、コードで最初の管理者を作れるはず', function($test) use ($makeIn, $readIn, $pass, $empty): void { $auth = $makeIn(); $test->assertFalse($auth->hasAdmin()); $test->assertEquals([], $auth->listUsers(), 'ログインしていなければ空'); $test->assertFalse($auth->setupFirstAdmin('x', 'boss0001', $pass, $pass, 'boss@example.com', '')->isSuccess, 'コードが無ければ作れない'); $res = $auth->setupFirstAdmin('chigau', 'boss0001', $pass, $pass, 'boss@example.com', 'himitsu-code'); $test->assertFalse($res->isSuccess); $test->assertStringContains('コード', (string)$res->message); $test->assertFalse(is_dir("{$empty}/user")); $res = @$makeIn()->setupFirstAdmin('himitsu-code', 'boss0001', $pass, $pass, 'boss@example.com', 'himitsu-code'); // CLIでは出力の後なのでクッキーを設定出来ないが、アカウントとトークンは保存される $test->assertTrue($res->isSuccess || $res->message === 'エラー:クッキーを設定に失敗。', (string)$res->message); $u = $readIn('boss0001'); $test->assertEquals(1, $u->id); $test->assertEquals(\Roles::MEMBER | \Roles::ADMIN, $u->role); $test->assertTrue(password_verify($pass, $u->password)); $test->assertCount(1, $u->tokens); $test->assertTrue(is_dir("{$empty}/ass/boss0001")); $test->assertTrue($makeIn('boss0001')->isAdmin()); $test->assertTrue($makeIn()->hasAdmin()); $test->assertFalse($makeIn()->setupFirstAdmin('himitsu-code', 'boss0002', $pass, $pass, 'b2@example.com', 'himitsu-code')->isSuccess, '2人目は作れない'); $makeIn()->clearLoginFailures(); }); $test->it('管理者はユーザーを作れるはず(他のユーザーは作れない)', function($test) use ($makeIn, $readIn, $pass): void { $admin = $makeIn('boss0001'); $res = $admin->adminCreateUser('member01', $pass, $pass, 'm1@example.com'); $test->assertTrue($res->isSuccess, (string)$res->message); $test->assertEquals(2, $readIn('member01')->id); $test->assertEquals(\Roles::MEMBER, $readIn('member01')->role); $test->assertFalse(isset($readIn('member01')->verified)); $res = $admin->adminCreateUser('staff001', $pass, $pass, 's1@example.com', [\Roles::MEMBER, \Roles::STAFF, 1 << 30]); $test->assertTrue($res->isSuccess, (string)$res->message); $test->assertEquals(\Roles::MEMBER | \Roles::STAFF, $readIn('staff001')->role, '不明なビットは無視'); $test->assertFalse($admin->adminCreateUser('member01', $pass, $pass, 'm9@example.com')->isSuccess, '同じユーザー名'); $test->assertFalse($admin->adminCreateUser('member09', $pass, $pass, 'm1@example.com')->isSuccess, '同じメールアドレス'); $test->assertFalse($admin->adminCreateUser('member09', 'short', 'short', 'm9@example.com')->isSuccess, '弱いパスワード'); $test->assertFalse($admin->adminCreateUser('mem', $pass, $pass, 'm9@example.com')->isSuccess, '短いユーザー名'); $test->assertFalse($admin->adminCreateUser('member09', $pass, $pass, 'not-an-email')->isSuccess); $test->assertFalse($makeIn('member01')->adminCreateUser('member09', $pass, $pass, 'm9@example.com')->isSuccess, '管理者以外'); $test->assertNull($readIn('member09')); }); $test->it('管理者だけがユーザーの一覧を見れるはず', function($test) use ($makeIn): void { $list = $makeIn('boss0001')->listUsers(); $test->assertEquals(['boss0001', 'member01', 'staff001'], array_column($list, 'username')); $test->assertEquals(1, $list[0]->sessions); $test->assertEquals(['メンバー', '管理者'], $list[0]->roleLabels); $test->assertFalse(isset($list[0]->password) || isset($list[0]->tokens)); $test->assertEquals([], $makeIn('member01')->listUsers()); $test->assertNull($makeIn('member01')->getAccount('boss0001')); $test->assertEquals('m1@example.com', $makeIn('boss0001')->getAccount('member01')->email); $test->assertNull($makeIn('boss0001')->getAccount('nobody01')); }); $test->it('管理者はアカウントを変更出来るはず(大事な変更には管理者のパスワード)', function($test) use ($makeIn, $readIn, $pass, $empty): void { $path = glob("{$empty}/user/*.member01.json")[0]; $u = $readIn('member01'); $u->tokens = [(object)['token' => str_repeat('a', 64), 'expDate' => time() + 3600, 'createDate' => time()]]; $u->verified = false; $u->verifyToken = 'x'; $u->pendingEmail = 'new@example.com'; $u->twoFactor = (object)['secret' => 'ABCDEFGH', 'lastStep' => 0]; file_put_contents($path, json_encode($u, JSON_PRETTY_PRINT)); $admin = $makeIn('boss0001'); $new = 'NewPASS56&*longenough!'; $test->assertFalse($makeIn('member01')->adminUpdateAccount('staff001', ['logoutAll' => true])->isSuccess, '管理者以外'); $test->assertFalse($admin->adminUpdateAccount('nobody01', ['logoutAll' => true])->isSuccess); $res = $admin->adminUpdateAccount('member01', ['email' => 'm1b@example.com'], 'machigatta'); $test->assertStringContains('管理者のパスワード', (string)$res->message); $test->assertFalse($admin->adminUpdateAccount('member01', ['disableTwoFactor' => true])->isSuccess); $test->assertFalse($admin->adminUpdateAccount('member01', ['password' => $new, 'passwordVerify' => 'x'], $pass)->isSuccess); $test->assertFalse($admin->adminUpdateAccount('member01', ['email' => 's1@example.com'], $pass)->isSuccess, '他のユーザーのメールアドレス'); $test->assertEquals('m1@example.com', $readIn('member01')->email, '失敗したら何も変えない'); $res = $admin->adminUpdateAccount('member01', ['email' => 'm1b@example.com', 'password' => $new, 'passwordVerify' => $new, 'disableTwoFactor' => true], $pass); $test->assertTrue($res->isSuccess, (string)$res->message); $u = $readIn('member01'); $test->assertEquals('m1b@example.com', $u->email); $test->assertFalse(isset($u->pendingEmail)); $test->assertTrue(password_verify($new, $u->password)); $test->assertCount(0, $u->tokens, 'パスワードを変えたら全部のログインを取り消す'); $test->assertFalse(isset($u->twoFactor)); // 確認済みにする(パスワードは不要) $res = $admin->adminUpdateAccount('member01', ['verify' => true]); $test->assertTrue($res->isSuccess, (string)$res->message); $u = $readIn('member01'); $test->assertTrue($u->verified); $test->assertFalse(isset($u->verifyToken)); $test->assertEquals('変更はありません。', $admin->adminUpdateAccount('member01', ['verify' => true, 'logoutAll' => true])->message); // ロール $res = $admin->adminUpdateAccount('member01', ['roles' => [\Roles::MEMBER, \Roles::FEDIVERSE]]); $test->assertTrue($res->isSuccess, (string)$res->message); $test->assertEquals(\Roles::MEMBER | \Roles::FEDIVERSE, $readIn('member01')->role); $test->assertTrue($readIn('member01')->fediDefault); $test->assertTrue($admin->adminUpdateAccount('member01', ['roles' => []])->isSuccess); $test->assertEquals(\Roles::BANNED, $readIn('member01')->role); $test->assertEquals(['BAN'], $admin->getAccount('member01')->roleLabels); $res = $admin->adminUpdateAccount('boss0001', ['roles' => [\Roles::MEMBER]]); $test->assertStringContains('最後の管理者', (string)$res->message); $test->assertTrue($makeIn('boss0001')->isAdmin()); // 全部のログインを取り消す $res = $admin->adminUpdateAccount('boss0001', ['logoutAll' => true]); $test->assertTrue($res->isSuccess, (string)$res->message); $test->assertCount(0, $readIn('boss0001')->tokens); }); }); // 一時ディレクトリを削除する $rm = function(string $dir) use (&$rm): void { foreach (scandir($dir) ?: [] as $f) { if ($f === '.' || $f === '..') continue; is_dir("{$dir}/{$f}") ? $rm("{$dir}/{$f}") : @unlink("{$dir}/{$f}"); } @rmdir($dir); }; $rm($base); $test->printSummary();