extend(TestCase::class) ->use(RefreshDatabase::class); beforeEach(function () { $this->service = app(PasswordGeneratorService::class); }); test('options fall back to the defaults when nothing is saved', function () { expect($this->service->options())->toBe([ 'mode' => 'button', 'type' => 'characters', 'length' => 20, 'characterSets' => ['uppercase', 'lowercase', 'numbers'], 'avoidAmbiguous' => true, 'words' => 6, 'separator' => 'hyphen', ]); }); test('options read the saved generator settings', function () { Setting::set('password_generator_mode', 'prefill'); Setting::set('password_generator_type', 'passphrase'); Setting::set('password_generator_length', 32); Setting::set('password_generator_character_sets', 'numbers,symbols'); Setting::set('password_generator_avoid_ambiguous', '0'); Setting::set('password_generator_words', 8); Setting::set('password_generator_separator', 'space'); expect($this->service->options())->toBe([ 'mode' => 'prefill', 'type' => 'passphrase', 'length' => 32, 'characterSets' => ['numbers', 'symbols'], 'avoidAmbiguous' => false, 'words' => 8, 'separator' => 'space', ]); }); test('options replace saved values that are not allowed with the defaults', function () { Setting::set('password_generator_mode', 'sometimes'); Setting::set('password_generator_type', 'emoji'); Setting::set('password_generator_length', 8); Setting::set('password_generator_character_sets', 'runes'); Setting::set('password_generator_words', 40); Setting::set('password_generator_separator', 'comma'); expect($this->service->options())->toMatchArray([ 'mode' => 'button', 'type' => 'characters', 'length' => 20, 'characterSets' => ['uppercase', 'lowercase', 'numbers'], 'words' => 6, 'separator' => 'hyphen', ]); }); test('a character password has the chosen length and only characters from the chosen sets', function () { $password = $this->service->generate([ 'type' => 'characters', 'length' => 40, 'characterSets' => ['numbers'], 'avoidAmbiguous' => true, 'words' => 6, 'separator' => 'hyphen', ]); expect($password)->toMatch('/^[2-9]{40}$/'); }); test('a character password holds at least one character of every chosen set', function () { foreach (range(1, 25) as $draw) { expect($this->service->characters(4, ['uppercase', 'lowercase', 'numbers', 'symbols'], false)) ->toMatch('/[A-Z]/') ->toMatch('/[a-z]/') ->toMatch('/[0-9]/') ->toMatch('/[^A-Za-z0-9]/'); } }); test('a character password leaves out look-alike characters when asked', function () { foreach (range(1, 25) as $draw) { expect($this->service->characters(64, ['uppercase', 'lowercase', 'numbers'], true))->not->toMatch('/[0O1lI]/'); } }); test('a character password needs room for every chosen set', function () { $this->service->characters(2, ['uppercase', 'lowercase', 'numbers'], false); })->throws(InvalidArgumentException::class); test('a passphrase has the chosen number of words from the word list, joined by the separator', function () { $wordList = file(resource_path('wordlists/eff-large-wordlist.txt'), FILE_IGNORE_NEW_LINES); $passphrase = $this->service->generate([ 'type' => 'passphrase', 'length' => 20, 'characterSets' => ['uppercase'], 'avoidAmbiguous' => true, 'words' => 7, 'separator' => 'dot', ]); $words = explode('.', $passphrase); expect($words)->toHaveCount(7); expect(array_diff($words, $wordList))->toBe([]); }); test('the word list is EFF\'s large list without its hyphenated words', function () { expect($this->service->wordList()) ->toHaveCount(7772) ->each->toMatch('/^[a-z]+$/'); }); test('two generated passwords differ', function () { expect($this->service->generate())->not->toBe($this->service->generate()); }); test('the entropy estimate follows the alphabet or the word list', function () { expect($this->service->entropyBits([ 'type' => 'characters', 'length' => 20, 'characterSets' => ['uppercase', 'lowercase', 'numbers'], 'avoidAmbiguous' => true, ]))->toBe(116); expect($this->service->entropyBits([ 'type' => 'passphrase', 'length' => 20, 'characterSets' => [], 'avoidAmbiguous' => false, 'words' => 6, ]))->toBe(77); });