[ ['username' => 'old', 'password' => password_hash('secret12', PASSWORD_DEFAULT)], ['username' => 'odd', 'password' => password_hash('secret12', PASSWORD_DEFAULT), 'role' => 'root'], ]])); $users = new UserStore($file); check('an account without a role is a user', $users->role('old') === 'user', $users->role('old')); check('an unknown role counts as user', $users->role('odd') === 'user', $users->role('odd')); check('an account that is not there is a user', $users->role('nobody') === 'user'); check('the old accounts have no creation date', $users->accounts()[1]['createdAt'] === '', $users->accounts()); $users->put('boss', 'secret12', 'admin'); check('a role given on creation is kept', $users->role('boss') === 'admin'); check('a new account gets a creation date', $users->accounts()[0]['createdAt'] !== '', $users->accounts()[0]); $created = $users->accounts()[0]['createdAt']; $users->put('boss', 'another12'); check('a new password without a role leaves the role alone', $users->role('boss') === 'admin'); check('a new password leaves the creation date alone', $users->accounts()[0]['createdAt'] === $created); check('the new password is the one that works', $users->verify('boss', 'another12') === 'boss'); $users->put('fresh', 'secret12'); check('a new account without a role is a user', $users->role('fresh') === 'user'); check('the role is written to the file', str_contains((string) file_get_contents($file), '"role": "admin"')); check('an unknown role is refused', throws(fn() => $users->put('x1', 'secret12', 'root'))); check( 'the accounts carry no hash', !str_contains(json_encode($users->accounts()), 'password') && !str_contains(json_encode($users->accounts()), '$2y$'), ); check('one administrator counted', $users->adminCount() === 1, $users->adminCount()); $users->setRole('fresh', 'admin'); check('setRole makes an administrator', $users->role('fresh') === 'admin' && $users->adminCount() === 2); check('setRole on nobody answers false', $users->setRole('nobody', 'user') === false); check('rename moves the account', $users->rename('fresh', 'renamed') && $users->exists('renamed') && !$users->exists('fresh')); check('rename keeps password and role', $users->verify('renamed', 'secret12') === 'renamed' && $users->role('renamed') === 'admin'); check('rename onto a taken name is refused', throws(fn() => $users->rename('renamed', 'OLD'))); check('the refused rename changed nothing', $users->exists('renamed') && $users->role('old') === 'user'); check('rename to an invalid name is refused', throws(fn() => $users->rename('renamed', 'a b'))); check('a change of case is the same account', $users->rename('renamed', 'Renamed') && $users->accounts()[0]['username'] === 'Renamed', $users->accounts()); check('rename of nobody answers false', $users->rename('nobody', 'somebody') === false); check('remove takes the account away', $users->remove('renamed') && !$users->exists('renamed')); check('remove of nobody answers false', $users->remove('renamed') === false); // --- Store: who reaches a project -------------------------------------------------------------- $projects = "{$dir}/projects"; $mine = new Store($projects, 'anna'); $doc = $mine->create('Anna\'s game'); $id = $doc['id']; $stranger = new Store($projects, 'bert'); $admin = new Store($projects, 'boss', true); check('a stranger does not see the project', $stranger->load($id) === null); check('a stranger cannot save it', $stranger->save($id, 1, ['name' => 'x']) === null); check('a stranger cannot delete it', $stranger->delete($id) === false); check('an administrator loads it', $admin->load($id)['name'] === "Anna's game"); $saved = $admin->save($id, 1, ['name' => 'Edited by boss']); check('an administrator saves it', $saved !== null && $saved['revision'] === 2, $saved); check('the save keeps the owner', $mine->load($id)['owner'] === 'anna', $mine->load($id)['owner'] ?? null); check('the owner sees the edit', $mine->load($id)['name'] === 'Edited by boss'); check('the administrator\'s own list stays their own', $admin->list() === []); check('listOf lists somebody else\'s projects', array_column($admin->listOf('anna'), 'id') === [$id]); check('transfer moves the project to the new name', $admin->transfer('anna', 'annabel') === 1); $moved = (new Store($projects, 'annabel'))->load($id); check('the new name owns it', $moved !== null && $moved['owner'] === 'annabel'); check('the old name does not', $mine->load($id) === null); check('transfer keeps the revision', $moved['revision'] === 2, $moved['revision'] ?? null); $other = (new Store($projects, 'carl'))->create('Carl\'s game'); check('an administrator deletes somebody else\'s project', $admin->delete($other['id']) && $admin->listOf('carl') === []); // --- AccountRules ------------------------------------------------------------------------------ check('renaming yourself is refused', AccountRules::renameRefusal('boss', 'BOSS') !== null); check('renaming somebody else is fine', AccountRules::renameRefusal('boss', 'anna') === null); check('changing your own role is refused', AccountRules::roleRefusal('boss', 'boss', 'admin', 'user', 2) !== null); check('setting your own role to what it is is fine', AccountRules::roleRefusal('boss', 'boss', 'admin', 'admin', 1) === null); check('the last administrator cannot be demoted', AccountRules::roleRefusal('x', 'boss', 'admin', 'user', 1) !== null); check('an administrator among two can be demoted', AccountRules::roleRefusal('x', 'boss', 'admin', 'user', 2) === null); check('deleting yourself is refused', AccountRules::deleteRefusal('boss', 'boss', 'admin', 2) !== null); check('the last administrator cannot be deleted', AccountRules::deleteRefusal('x', 'boss', 'admin', 1) !== null); check('a user can be deleted', AccountRules::deleteRefusal('boss', 'anna', 'user', 1) === null); check('an account with projects is not deleted', AccountRules::ownerRefusal('anna', 2) === 'anna still owns 2 projects'); check('one project is said as one', AccountRules::ownerRefusal('anna', 1) === 'anna still owns 1 project'); check('an account without projects may go', AccountRules::ownerRefusal('anna', 0) === null); exec('rm -rf ' . escapeshellarg($dir)); echo $failed === 0 ? "ALL PASSED – {$passed} checks\n" : "{$failed} FAILED, {$passed} passed\n"; exit($failed === 0 ? 0 : 1);