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();