diff --git a/app/Api/v1/Requests/TwoFAccountDynamicRequest.php b/app/Api/v1/Requests/TwoFAccountDynamicRequest.php index daba1eb2..c6914fa9 100644 --- a/app/Api/v1/Requests/TwoFAccountDynamicRequest.php +++ b/app/Api/v1/Requests/TwoFAccountDynamicRequest.php @@ -35,6 +35,8 @@ public function rules() /** * Prepare the data for validation. * + * @codeCoverageIgnore + * * @return void */ protected function prepareForValidation() diff --git a/app/Api/v1/Requests/TwoFAccountStoreRequest.php b/app/Api/v1/Requests/TwoFAccountStoreRequest.php index 7736a16e..b6a3e9c6 100644 --- a/app/Api/v1/Requests/TwoFAccountStoreRequest.php +++ b/app/Api/v1/Requests/TwoFAccountStoreRequest.php @@ -40,6 +40,8 @@ public function rules() /** * Prepare the data for validation. * + * @codeCoverageIgnore + * * @return void */ protected function prepareForValidation() diff --git a/app/Api/v1/Requests/TwoFAccountUpdateRequest.php b/app/Api/v1/Requests/TwoFAccountUpdateRequest.php index 6665c9c2..473bd707 100644 --- a/app/Api/v1/Requests/TwoFAccountUpdateRequest.php +++ b/app/Api/v1/Requests/TwoFAccountUpdateRequest.php @@ -40,6 +40,8 @@ public function rules() /** * Prepare the data for validation. * + * @codeCoverageIgnore + * * @return void */ protected function prepareForValidation() diff --git a/app/Api/v1/Requests/TwoFAccountUriRequest.php b/app/Api/v1/Requests/TwoFAccountUriRequest.php index 2798f0dd..c383fa46 100644 --- a/app/Api/v1/Requests/TwoFAccountUriRequest.php +++ b/app/Api/v1/Requests/TwoFAccountUriRequest.php @@ -33,6 +33,8 @@ public function rules() /** * Prepare the data for validation. * + * @codeCoverageIgnore + * * @return void */ protected function prepareForValidation() diff --git a/app/Extensions/WebauthnCredentialBroker.php b/app/Extensions/WebauthnCredentialBroker.php index 35379c46..4b1cc83d 100644 --- a/app/Extensions/WebauthnCredentialBroker.php +++ b/app/Extensions/WebauthnCredentialBroker.php @@ -31,7 +31,7 @@ public function sendResetLink(array $credentials, Closure $callback = null) : st $token = $this->tokens->create($user); if ($callback) { - $callback($user, $token); + $callback($user, $token); // @codeCoverageIgnore } else { $user->sendWebauthnRecoveryNotification($token); } diff --git a/app/Helpers/Helpers.php b/app/Helpers/Helpers.php index 34301009..ef5c502f 100644 --- a/app/Helpers/Helpers.php +++ b/app/Helpers/Helpers.php @@ -25,6 +25,7 @@ public static function getUniqueFilename(string $extension) : string */ public static function cleanVersionNumber(?string $release) : string|false { - return preg_match('/([[0-9][0-9\.]*[0-9])/', $release, $version) ? $version[0] : false; + // We use the regex for semver detection (see https://semver.org/) + return preg_match('/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/', $release, $version) ? $version[0] : false; } } diff --git a/app/Http/Controllers/Auth/WebAuthnLoginController.php b/app/Http/Controllers/Auth/WebAuthnLoginController.php index ca519947..9a59406f 100644 --- a/app/Http/Controllers/Auth/WebAuthnLoginController.php +++ b/app/Http/Controllers/Auth/WebAuthnLoginController.php @@ -11,6 +11,7 @@ use Laragear\WebAuthn\Http\Requests\AssertedRequest; use Laragear\WebAuthn\Http\Requests\AssertionRequest; use Laragear\WebAuthn\WebAuthn; +use Illuminate\Support\Arr; class WebAuthnLoginController extends Controller { @@ -33,7 +34,7 @@ class WebAuthnLoginController extends Controller */ public function options(AssertionRequest $request) : Responsable|JsonResponse { - switch (env('WEBAUTHN_USER_VERIFICATION')) { + switch (config('webauthn.user_verification')) { case WebAuthn::USER_VERIFICATION_DISCOURAGED: $request = $request->fastLogin(); // Makes the authenticator to only check for user presence on registration break; @@ -69,7 +70,7 @@ public function login(AssertedRequest $request) // Some authenticators do not send a userHandle so we hack the response to be compliant // with Larapass/webauthn-lib implementation that waits for a userHandle - if (! $response['userHandle']) { + if (!Arr::exists($response, 'userHandle') || blank($response['userHandle'])) { $response['userHandle'] = User::getFromCredentialId($request->id)?->userHandle(); $request->merge(['response' => $response]); } diff --git a/app/Http/Controllers/Auth/WebAuthnRegisterController.php b/app/Http/Controllers/Auth/WebAuthnRegisterController.php index 16109f4a..5c014ef8 100644 --- a/app/Http/Controllers/Auth/WebAuthnRegisterController.php +++ b/app/Http/Controllers/Auth/WebAuthnRegisterController.php @@ -19,7 +19,7 @@ class WebAuthnRegisterController extends Controller */ public function options(AttestationRequest $request) : Responsable { - switch (env('WEBAUTHN_USER_VERIFICATION')) { + switch (config('webauthn.user_verification')) { case WebAuthn::USER_VERIFICATION_DISCOURAGED: $request = $request->fastRegistration(); // Makes the authenticator to only check for user presence on registration break; diff --git a/app/Http/Middleware/TrustHosts.php b/app/Http/Middleware/TrustHosts.php index 7186414c..98c1a240 100644 --- a/app/Http/Middleware/TrustHosts.php +++ b/app/Http/Middleware/TrustHosts.php @@ -4,6 +4,9 @@ use Illuminate\Http\Middleware\TrustHosts as Middleware; +/** + * @codeCoverageIgnore + */ class TrustHosts extends Middleware { /** diff --git a/app/Models/TwoFAccount.php b/app/Models/TwoFAccount.php index aa17756e..90fcd0c8 100644 --- a/app/Models/TwoFAccount.php +++ b/app/Models/TwoFAccount.php @@ -60,8 +60,6 @@ class TwoFAccount extends Model implements Sortable const FAKE_ID = -2; - private const IMAGELINK_STORAGE_PATH = 'imagesLink/'; - /** * List of OTP types supported by 2FAuth */ @@ -376,10 +374,6 @@ public function fillWithOtpParameters(array $parameters, bool $skipIconFetching $this->enforceAsSteam(); } - if (! $this->icon && $skipIconFetching) { - $this->icon = $this->getDefaultIcon(); - } - if (! $this->icon && Settings::get('getOfficialIcons') && ! $skipIconFetching) { $this->icon = $this->getDefaultIcon(); } @@ -441,6 +435,22 @@ public function fillWithURI(string $uri, bool $isSteamTotp = false, bool $skipIc return $this; } + /** + * Compare 2 TwoFAccounts + */ + public function equals(self $other): bool + { + return $this->service === $other->service && + $this->account === $other->account && + $this->icon === $other->icon && + $this->otp_type === $other->otp_type && + $this->secret === $other->secret && + $this->digits === $other->digits && + $this->algorithm === $other->algorithm && + $this->period === $other->period && + $this->counter === $other->counter; + } + /** * Sets model attributes to STEAM values */ @@ -534,7 +544,6 @@ private function storeImageAsIcon(string $url) try { $path_parts = pathinfo($url); $newFilename = Helpers::getUniqueFilename($path_parts['extension']); - $imageFile = self::IMAGELINK_STORAGE_PATH . $newFilename; try { $response = Http::retry(3, 100)->get($url); @@ -546,8 +555,10 @@ private function storeImageAsIcon(string $url) Log::error(sprintf('Cannot fetch imageLink at "%s"', $url)); } - if (in_array(Storage::mimeType($imageFile), ['image/png', 'image/jpeg', 'image/webp', 'image/bmp']) - && getimagesize(storage_path() . '/app/' . $imageFile)) { + if ( + in_array(Storage::disk('imagesLink')->mimeType($newFilename), ['image/png', 'image/jpeg', 'image/webp', 'image/bmp']) + && getimagesize(Storage::disk('imagesLink')->path($newFilename)) + ) { // Should be a valid image, we move it to the icons disk if (Storage::disk('icons')->put($newFilename, Storage::disk('imagesLink')->get($newFilename))) { Storage::disk('imagesLink')->delete($newFilename); @@ -555,10 +566,8 @@ private function storeImageAsIcon(string $url) Log::info(sprintf('Icon file %s stored', $newFilename)); } else { - // @codeCoverageIgnoreStart Storage::disk('imagesLink')->delete($newFilename); throw new \Exception('Unsupported mimeType or missing image on storage'); - // @codeCoverageIgnoreEnd } return $newFilename; diff --git a/app/Providers/TwoFAuthServiceProvider.php b/app/Providers/TwoFAuthServiceProvider.php index 3322c184..01a13d70 100644 --- a/app/Providers/TwoFAuthServiceProvider.php +++ b/app/Providers/TwoFAuthServiceProvider.php @@ -49,6 +49,7 @@ public function boot() /** * Get the services provided by the provider. * + * @codeCoverageIgnore * @return array */ public function provides() diff --git a/app/Services/Migrators/GoogleAuthMigrator.php b/app/Services/Migrators/GoogleAuthMigrator.php index a69e3272..bdb1f634 100644 --- a/app/Services/Migrators/GoogleAuthMigrator.php +++ b/app/Services/Migrators/GoogleAuthMigrator.php @@ -59,7 +59,7 @@ public function migrate(mixed $migrationPayload) : Collection // The token failed to generate a valid account so we create a fake account to be returned. $fakeAccount = new TwoFAccount(); - $fakeAccount->id = -2; + $fakeAccount->id = TwoFAccount::FAKE_ID; $fakeAccount->otp_type = $fakeAccount::TOTP; // Only basic fields are filled to limit the risk of another exception. $fakeAccount->account = $otp_parameters->getName() ?? __('twofaccounts.import.invalid_account'); diff --git a/app/Services/Migrators/PlainTextMigrator.php b/app/Services/Migrators/PlainTextMigrator.php index 86a34ff7..9dc116f7 100644 --- a/app/Services/Migrators/PlainTextMigrator.php +++ b/app/Services/Migrators/PlainTextMigrator.php @@ -39,7 +39,7 @@ public function migrate(mixed $migrationPayload) : Collection // The token failed to generate a valid account so we create a fake account to be returned. $fakeAccount = new TwoFAccount(); - $fakeAccount->id = -2; + $fakeAccount->id = TwoFAccount::FAKE_ID; $fakeAccount->otp_type = substr($uri, 10, 4); // Only basic fields are filled to limit the risk of another exception. $fakeAccount->account = __('twofaccounts.import.invalid_account'); diff --git a/app/Services/Migrators/TwoFASMigrator.php b/app/Services/Migrators/TwoFASMigrator.php index 506fa0ac..30e17324 100644 --- a/app/Services/Migrators/TwoFASMigrator.php +++ b/app/Services/Migrators/TwoFASMigrator.php @@ -89,8 +89,12 @@ public function migrate(mixed $migrationPayload) : Collection $parameters['secret'] = $this->padToValidBase32Secret($otp_parameters['secret']); $parameters['algorithm'] = $otp_parameters['otp']['algorithm']; $parameters['digits'] = $otp_parameters['otp']['digits']; - $parameters['counter'] = $otp_parameters['otp']['counter'] ?? null; - $parameters['period'] = $otp_parameters['otp']['period'] ?? null; + $parameters['counter'] = strtolower($parameters['otp_type']) === 'hotp' && $otp_parameters['otp']['counter'] > 0 + ? $otp_parameters['otp']['counter'] + : null; + $parameters['period'] = strtolower($parameters['otp_type']) === 'totp' && $otp_parameters['otp']['period'] > 0 + ? $otp_parameters['otp']['period'] + : null; try { $twofaccounts[$key] = new TwoFAccount; diff --git a/app/Services/ReleaseRadarService.php b/app/Services/ReleaseRadarService.php index af54a6ea..17004035 100644 --- a/app/Services/ReleaseRadarService.php +++ b/app/Services/ReleaseRadarService.php @@ -16,7 +16,7 @@ class ReleaseRadarService */ public function scheduledScan() : void { - if ((Settings::get('lastRadarScan') + 604800) < time()) { + if ((Settings::get('lastRadarScan') + (60 * 60 * 24 * 7)) < time()) { $this->newRelease(); } } @@ -39,18 +39,19 @@ public function manualScan() : false|string protected function newRelease() : false|string { if ($latestReleaseData = json_decode($this->getLatestReleaseData())) { + $githubVersion = Helpers::cleanVersionNumber($latestReleaseData->tag_name); $installedVersion = Helpers::cleanVersionNumber(config('2fauth.version')); - if ($githubVersion > $installedVersion && $latestReleaseData->prerelease == false && $latestReleaseData->draft == false) { - Settings::set('latestRelease', $latestReleaseData->tag_name); + if ($githubVersion && $installedVersion) { + if ($githubVersion > $installedVersion && $latestReleaseData->prerelease == false && $latestReleaseData->draft == false) { + Settings::set('latestRelease', $latestReleaseData->tag_name); - return $latestReleaseData->tag_name; - } else { - Settings::delete('latestRelease'); + return $latestReleaseData->tag_name; + } else { + Settings::delete('latestRelease'); + } } - - Settings::set('lastRadarScan', time()); } return false; @@ -68,6 +69,8 @@ protected function getLatestReleaseData() : string|null ->get(config('2fauth.latestReleaseUrl')); if ($response->successful()) { + Settings::set('lastRadarScan', time()); + return $response->body(); } } catch (\Exception $exception) { diff --git a/config/webauthn.php b/config/webauthn.php index c54e5a26..3214fff0 100644 --- a/config/webauthn.php +++ b/config/webauthn.php @@ -2,6 +2,8 @@ return [ + 'user_verification' => env('WEBAUTHN_USER_VERIFICATION', 'discouraged'), + /* |-------------------------------------------------------------------------- | Relaying Party diff --git a/tests/Api/v1/Controllers/Auth/UserControllerTest.php b/tests/Api/v1/Controllers/Auth/UserControllerTest.php index 6abaab92..f2d13940 100644 --- a/tests/Api/v1/Controllers/Auth/UserControllerTest.php +++ b/tests/Api/v1/Controllers/Auth/UserControllerTest.php @@ -5,6 +5,10 @@ use App\Models\User; use Tests\FeatureTestCase; +/** + * @covers \App\Api\v1\Controllers\UserController + * @covers \App\Api\v1\Resources\UserResource + */ class UserControllerTest extends FeatureTestCase { /** @@ -15,7 +19,7 @@ class UserControllerTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); diff --git a/tests/Api/v1/Controllers/GroupControllerTest.php b/tests/Api/v1/Controllers/GroupControllerTest.php index eb0926d4..78f9d6fb 100644 --- a/tests/Api/v1/Controllers/GroupControllerTest.php +++ b/tests/Api/v1/Controllers/GroupControllerTest.php @@ -21,7 +21,7 @@ class GroupControllerTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); diff --git a/tests/Api/v1/Controllers/IconControllerTest.php b/tests/Api/v1/Controllers/IconControllerTest.php index 29222750..8afdd07b 100644 --- a/tests/Api/v1/Controllers/IconControllerTest.php +++ b/tests/Api/v1/Controllers/IconControllerTest.php @@ -40,6 +40,31 @@ public function test_upload_with_invalid_data_returns_validation_error() ->assertStatus(422); } + /** + * @test + */ + public function test_fetch_logo_returns_filename() + { + $response = $this->json('POST', '/api/v1/icons/default', [ + 'service' => 'twitter', + ]) + ->assertStatus(201) + ->assertJsonStructure([ + 'filename', + ]); + } + + /** + * @test + */ + public function test_fetch_unknown_logo_returns_nothing() + { + $response = $this->json('POST', '/api/v1/icons/default', [ + 'service' => 'unknown_company', + ]) + ->assertNoContent(); + } + /** * @test */ diff --git a/tests/Api/v1/Controllers/TwoFAccountControllerTest.php b/tests/Api/v1/Controllers/TwoFAccountControllerTest.php index fb28fa51..a83d7445 100644 --- a/tests/Api/v1/Controllers/TwoFAccountControllerTest.php +++ b/tests/Api/v1/Controllers/TwoFAccountControllerTest.php @@ -9,13 +9,16 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; use Tests\Classes\LocalFile; -use Tests\Classes\OtpTestData; +use Tests\Data\OtpTestData; use Tests\FeatureTestCase; +use Tests\Data\MigrationTestData; /** * @covers \App\Api\v1\Controllers\TwoFAccountController * @covers \App\Api\v1\Resources\TwoFAccountReadResource * @covers \App\Api\v1\Resources\TwoFAccountStoreResource + * @covers \App\Providers\MigrationServiceProvider + * @covers \App\Providers\TwoFAuthServiceProvider */ class TwoFAccountControllerTest extends FeatureTestCase { @@ -122,7 +125,7 @@ class TwoFAccountControllerTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); @@ -447,7 +450,7 @@ public function test_import_valid_gauth_payload_returns_success_with_consistent_ { $response = $this->actingAs($this->user, 'api-guard') ->json('POST', '/api/v1/twofaccounts/migration', [ - 'payload' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI, + 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI, 'withSecret' => 1, ]) ->assertOk() @@ -483,7 +486,7 @@ public function test_import_with_invalid_gauth_payload_returns_validation_error( { $response = $this->actingAs($this->user, 'api-guard') ->json('POST', '/api/v1/twofaccounts/migration', [ - 'uri' => OtpTestData::INVALID_GOOGLE_AUTH_MIGRATION_URI, + 'uri' => MigrationTestData::INVALID_GOOGLE_AUTH_MIGRATION_URI, ]) ->assertStatus(422); } @@ -507,7 +510,7 @@ public function test_import_gauth_payload_with_duplicates_returns_negative_ids() $response = $this->actingAs($this->user, 'api-guard') ->json('POST', '/api/v1/twofaccounts/migration', [ - 'payload' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI, + 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI, ]) ->assertOk() ->assertJsonFragment([ @@ -524,7 +527,7 @@ public function test_import_invalid_gauth_payload_returns_bad_request() { $response = $this->actingAs($this->user, 'api-guard') ->json('POST', '/api/v1/twofaccounts/migration', [ - 'payload' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA, + 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA, ]) ->assertStatus(400) ->assertJsonStructure([ @@ -546,22 +549,11 @@ public function test_import_valid_aegis_json_file_returns_success() 'withSecret' => 1, ]) ->assertOk() - ->assertJsonCount(5, $key = null) + ->assertJsonCount(3, $key = null) ->assertJsonFragment([ 'id' => 0, - 'service' => OtpTestData::SERVICE . '_totp', - 'account' => OtpTestData::ACCOUNT . '_totp', - 'otp_type' => 'totp', - 'secret' => OtpTestData::SECRET, - 'digits' => OtpTestData::DIGITS_DEFAULT, - 'algorithm' => OtpTestData::ALGORITHM_DEFAULT, - 'period' => OtpTestData::PERIOD_DEFAULT, - 'counter' => null, - ]) - ->assertJsonFragment([ - 'id' => 0, - 'service' => OtpTestData::SERVICE . '_totp_custom', - 'account' => OtpTestData::ACCOUNT . '_totp_custom', + 'service' => OtpTestData::SERVICE, + 'account' => OtpTestData::ACCOUNT, 'otp_type' => 'totp', 'secret' => OtpTestData::SECRET, 'digits' => OtpTestData::DIGITS_CUSTOM, @@ -571,21 +563,10 @@ public function test_import_valid_aegis_json_file_returns_success() ]) ->assertJsonFragment([ 'id' => 0, - 'service' => OtpTestData::SERVICE . '_hotp', - 'account' => OtpTestData::ACCOUNT . '_hotp', + 'service' => OtpTestData::SERVICE, + 'account' => OtpTestData::ACCOUNT, 'otp_type' => 'hotp', 'secret' => OtpTestData::SECRET, - 'digits' => OtpTestData::DIGITS_DEFAULT, - 'algorithm' => OtpTestData::ALGORITHM_DEFAULT, - 'period' => null, - 'counter' => OtpTestData::COUNTER_DEFAULT, - ]) - ->assertJsonFragment([ - 'id' => 0, - 'service' => OtpTestData::SERVICE . '_hotp_custom', - 'account' => OtpTestData::ACCOUNT . '_hotp_custom', - 'otp_type' => 'totp', - 'secret' => OtpTestData::SECRET, 'digits' => OtpTestData::DIGITS_CUSTOM, 'algorithm' => OtpTestData::ALGORITHM_CUSTOM, 'period' => null, @@ -594,7 +575,7 @@ public function test_import_valid_aegis_json_file_returns_success() ->assertJsonFragment([ 'id' => 0, 'service' => OtpTestData::STEAM, - 'account' => OtpTestData::ACCOUNT . '_steam', + 'account' => OtpTestData::ACCOUNT, 'otp_type' => 'steamtotp', 'secret' => OtpTestData::STEAM_SECRET, 'digits' => OtpTestData::DIGITS_STEAM, @@ -625,10 +606,10 @@ public function test_import_invalid_aegis_json_file_returns_bad_request($file) public function invalidAegisJsonFileProvider() { return [ - 'validPlainTextFile' => [ + 'encryptedAegisJsonFile' => [ LocalFile::fake()->encryptedAegisJsonFile(), ], - 'validPlainTextFileWithNewLines' => [ + 'invalidAegisJsonFile' => [ LocalFile::fake()->invalidAegisJsonFile(), ], ]; @@ -720,16 +701,16 @@ public function test_import_invalid_plain_text_file_returns_bad_request($file) public function invalidPlainTextFileProvider() { return [ - 'validPlainTextFile' => [ + 'invalidPlainTextFileEmpty' => [ LocalFile::fake()->invalidPlainTextFileEmpty(), ], - 'validPlainTextFileWithNewLines' => [ + 'invalidPlainTextFileNoUri' => [ LocalFile::fake()->invalidPlainTextFileNoUri(), ], - 'validPlainTextFileWithNewLines' => [ + 'invalidPlainTextFileWithInvalidUri' => [ LocalFile::fake()->invalidPlainTextFileWithInvalidUri(), ], - 'validPlainTextFileWithNewLines' => [ + 'invalidPlainTextFileWithInvalidLine' => [ LocalFile::fake()->invalidPlainTextFileWithInvalidLine(), ], ]; @@ -744,7 +725,8 @@ public function test_reorder_returns_success() $response = $this->actingAs($this->user, 'api-guard') ->json('POST', '/api/v1/twofaccounts/reorder', [ - 'orderedIds' => [3, 2, 1], ]) + 'orderedIds' => [3, 2, 1], + ]) ->assertStatus(200) ->assertJsonStructure([ 'message', @@ -760,7 +742,8 @@ public function test_reorder_with_invalid_data_returns_validation_error() $response = $this->actingAs($this->user, 'api-guard') ->json('POST', '/api/v1/twofaccounts/reorder', [ - 'orderedIds' => '3,2,1', ]) + 'orderedIds' => '3,2,1', + ]) ->assertStatus(422); } diff --git a/tests/Api/v1/Requests/GroupAssignRequestTest.php b/tests/Api/v1/Requests/GroupAssignRequestTest.php index 79d7ed0a..5ac1e04b 100644 --- a/tests/Api/v1/Requests/GroupAssignRequestTest.php +++ b/tests/Api/v1/Requests/GroupAssignRequestTest.php @@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Validator; use Tests\TestCase; +/** + * @covers \App\Api\v1\Requests\GroupAssignRequest + */ class GroupAssignRequestTest extends TestCase { use WithoutMiddleware; @@ -18,8 +21,8 @@ class GroupAssignRequestTest extends TestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new GroupAssignRequest(); @@ -29,7 +32,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new GroupAssignRequest(); $validator = Validator::make($data, $request->rules()); @@ -40,7 +43,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { return [ [[ @@ -54,7 +57,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { $request = new GroupAssignRequest(); $validator = Validator::make($data, $request->rules()); @@ -65,7 +68,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Api/v1/Requests/GroupStoreRequestTest.php b/tests/Api/v1/Requests/GroupStoreRequestTest.php index ac33bc13..ba81e6ae 100644 --- a/tests/Api/v1/Requests/GroupStoreRequestTest.php +++ b/tests/Api/v1/Requests/GroupStoreRequestTest.php @@ -9,6 +9,9 @@ use Illuminate\Support\Facades\Validator; use Tests\FeatureTestCase; +/** + * @covers \App\Api\v1\Requests\GroupStoreRequest + */ class GroupStoreRequestTest extends FeatureTestCase { use WithoutMiddleware; @@ -21,8 +24,8 @@ class GroupStoreRequestTest extends FeatureTestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new GroupStoreRequest(); @@ -32,7 +35,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new GroupStoreRequest(); $validator = Validator::make($data, $request->rules()); @@ -43,7 +46,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { return [ [[ @@ -55,7 +58,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { $group = new Group([ 'name' => $this->uniqueGroupName, @@ -72,7 +75,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Api/v1/Requests/QrCodeDecodeRequestTest.php b/tests/Api/v1/Requests/QrCodeDecodeRequestTest.php index 6c9e3c49..f6408aa3 100644 --- a/tests/Api/v1/Requests/QrCodeDecodeRequestTest.php +++ b/tests/Api/v1/Requests/QrCodeDecodeRequestTest.php @@ -9,6 +9,9 @@ use Tests\Classes\LocalFile; use Tests\TestCase; +/** + * @covers \App\Api\v1\Requests\QrCodeDecodeRequest + */ class QrCodeDecodeRequestTest extends TestCase { use WithoutMiddleware; @@ -19,8 +22,8 @@ class QrCodeDecodeRequestTest extends TestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new QrCodeDecodeRequest(); @@ -30,7 +33,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new QrCodeDecodeRequest(); $validator = Validator::make($data, $request->rules()); @@ -41,7 +44,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { $file = LocalFile::fake()->validQrcode(); @@ -55,7 +58,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { $request = new QrCodeDecodeRequest(); $validator = Validator::make($data, $request->rules()); @@ -66,7 +69,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Api/v1/Requests/SettingStoreRequestTest.php b/tests/Api/v1/Requests/SettingStoreRequestTest.php index b82fda55..a25154a7 100644 --- a/tests/Api/v1/Requests/SettingStoreRequestTest.php +++ b/tests/Api/v1/Requests/SettingStoreRequestTest.php @@ -9,6 +9,9 @@ use Illuminate\Support\Facades\Validator; use Tests\FeatureTestCase; +/** + * @covers \App\Api\v1\Requests\SettingStoreRequest + */ class SettingStoreRequestTest extends FeatureTestCase { use WithoutMiddleware; @@ -21,8 +24,8 @@ class SettingStoreRequestTest extends FeatureTestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new SettingStoreRequest(); @@ -32,7 +35,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new SettingStoreRequest(); $validator = Validator::make($data, $request->rules()); @@ -43,7 +46,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { return [ [[ @@ -64,7 +67,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { Settings::set($this->uniqueKey, 'uniqueValue'); @@ -77,7 +80,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Api/v1/Requests/SettingUpdateRequestTest.php b/tests/Api/v1/Requests/SettingUpdateRequestTest.php index 5221c068..f1e7ba02 100644 --- a/tests/Api/v1/Requests/SettingUpdateRequestTest.php +++ b/tests/Api/v1/Requests/SettingUpdateRequestTest.php @@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Validator; use Tests\TestCase; +/** + * @covers \App\Api\v1\Requests\SettingUpdateRequest + */ class SettingUpdateRequestTest extends TestCase { use WithoutMiddleware; @@ -18,8 +21,8 @@ class SettingUpdateRequestTest extends TestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new SettingUpdateRequest(); @@ -29,7 +32,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new SettingUpdateRequest(); $validator = Validator::make($data, $request->rules()); @@ -40,7 +43,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { return [ [[ @@ -58,7 +61,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { $request = new SettingUpdateRequest(); $validator = Validator::make($data, $request->rules()); @@ -69,7 +72,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Api/v1/Requests/TwoFAccountBatchRequestTest.php b/tests/Api/v1/Requests/TwoFAccountBatchRequestTest.php index 7b2958e4..eff74e8e 100644 --- a/tests/Api/v1/Requests/TwoFAccountBatchRequestTest.php +++ b/tests/Api/v1/Requests/TwoFAccountBatchRequestTest.php @@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Validator; use Tests\TestCase; +/** + * @covers \App\Api\v1\Requests\TwoFAccountBatchRequest + */ class TwoFAccountBatchRequestTest extends TestCase { use WithoutMiddleware; @@ -18,8 +21,8 @@ class TwoFAccountBatchRequestTest extends TestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new TwoFAccountBatchRequest(); @@ -29,7 +32,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new TwoFAccountBatchRequest(); $validator = Validator::make($data, $request->rules()); @@ -40,7 +43,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { return [ [[ @@ -55,7 +58,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { $request = new TwoFAccountBatchRequest(); $validator = Validator::make($data, $request->rules()); @@ -66,7 +69,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Api/v1/Requests/TwoFAccountDynamicRequestTest.php b/tests/Api/v1/Requests/TwoFAccountDynamicRequestTest.php index 2be0b529..4f2efb91 100644 --- a/tests/Api/v1/Requests/TwoFAccountDynamicRequestTest.php +++ b/tests/Api/v1/Requests/TwoFAccountDynamicRequestTest.php @@ -9,6 +9,9 @@ use Illuminate\Support\Facades\Auth; use Tests\TestCase; +/** + * @covers \App\Api\v1\Requests\TwoFAccountDynamicRequest + */ class TwoFAccountDynamicRequestTest extends TestCase { use WithoutMiddleware; @@ -19,8 +22,8 @@ class TwoFAccountDynamicRequestTest extends TestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new TwoFAccountDynamicRequest(); diff --git a/tests/Api/v1/Requests/TwoFAccountImportRequestTest.php b/tests/Api/v1/Requests/TwoFAccountImportRequestTest.php index 07a48e18..77199e43 100644 --- a/tests/Api/v1/Requests/TwoFAccountImportRequestTest.php +++ b/tests/Api/v1/Requests/TwoFAccountImportRequestTest.php @@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Validator; use Tests\TestCase; +/** + * @covers \App\Api\v1\Requests\TwoFAccountImportRequest + */ class TwoFAccountImportRequestTest extends TestCase { use WithoutMiddleware; @@ -18,8 +21,8 @@ class TwoFAccountImportRequestTest extends TestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new TwoFAccountImportRequest(); @@ -29,7 +32,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new TwoFAccountImportRequest(); $validator = Validator::make($data, $request->rules()); @@ -40,7 +43,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { return [ [[ @@ -52,7 +55,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { $request = new TwoFAccountImportRequest(); $validator = Validator::make($data, $request->rules()); @@ -63,7 +66,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Api/v1/Requests/TwoFAccountReorderRequestTest.php b/tests/Api/v1/Requests/TwoFAccountReorderRequestTest.php index 82c967cd..2a546dc1 100644 --- a/tests/Api/v1/Requests/TwoFAccountReorderRequestTest.php +++ b/tests/Api/v1/Requests/TwoFAccountReorderRequestTest.php @@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Validator; use Tests\TestCase; +/** + * @covers \App\Api\v1\Requests\TwoFAccountReorderRequest + */ class TwoFAccountReorderRequestTest extends TestCase { use WithoutMiddleware; @@ -18,8 +21,8 @@ class TwoFAccountReorderRequestTest extends TestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new TwoFAccountReorderRequest(); @@ -29,7 +32,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new TwoFAccountReorderRequest(); $validator = Validator::make($data, $request->rules()); @@ -40,7 +43,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { return [ [[ @@ -55,7 +58,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { $request = new TwoFAccountReorderRequest(); $validator = Validator::make($data, $request->rules()); @@ -66,7 +69,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Api/v1/Requests/TwoFAccountStoreRequestTest.php b/tests/Api/v1/Requests/TwoFAccountStoreRequestTest.php index b42898ad..38c7d1a2 100644 --- a/tests/Api/v1/Requests/TwoFAccountStoreRequestTest.php +++ b/tests/Api/v1/Requests/TwoFAccountStoreRequestTest.php @@ -8,6 +8,10 @@ use Illuminate\Support\Facades\Validator; use Tests\TestCase; +/** + * @covers \App\Api\v1\Requests\TwoFAccountStoreRequest + * @covers \App\Rules\IsBase32Encoded + */ class TwoFAccountStoreRequestTest extends TestCase { use WithoutMiddleware; @@ -18,8 +22,8 @@ class TwoFAccountStoreRequestTest extends TestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new TwoFAccountStoreRequest(); @@ -29,7 +33,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new TwoFAccountStoreRequest(); $validator = Validator::make($data, $request->rules()); @@ -40,7 +44,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { return [ [[ @@ -98,7 +102,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { $request = new TwoFAccountStoreRequest(); $validator = Validator::make($data, $request->rules()); @@ -109,7 +113,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Api/v1/Requests/TwoFAccountUpdateRequestTest.php b/tests/Api/v1/Requests/TwoFAccountUpdateRequestTest.php index 50ea1e7f..890b5bd0 100644 --- a/tests/Api/v1/Requests/TwoFAccountUpdateRequestTest.php +++ b/tests/Api/v1/Requests/TwoFAccountUpdateRequestTest.php @@ -8,6 +8,10 @@ use Illuminate\Support\Facades\Validator; use Tests\TestCase; +/** + * @covers \App\Api\v1\Requests\TwoFAccountUpdateRequest + * @covers \App\Rules\IsBase32Encoded + */ class TwoFAccountUpdateRequestTest extends TestCase { use WithoutMiddleware; @@ -18,8 +22,8 @@ class TwoFAccountUpdateRequestTest extends TestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new TwoFAccountUpdateRequest(); @@ -29,7 +33,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new TwoFAccountUpdateRequest(); $validator = Validator::make($data, $request->rules()); @@ -40,7 +44,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { return [ [[ @@ -80,7 +84,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { $request = new TwoFAccountUpdateRequest(); $validator = Validator::make($data, $request->rules()); @@ -91,7 +95,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Api/v1/Requests/TwoFAccountUriRequestTest.php b/tests/Api/v1/Requests/TwoFAccountUriRequestTest.php index 68faa283..ee1b0b5c 100644 --- a/tests/Api/v1/Requests/TwoFAccountUriRequestTest.php +++ b/tests/Api/v1/Requests/TwoFAccountUriRequestTest.php @@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Validator; use Tests\TestCase; +/** + * @covers \App\Api\v1\Requests\TwoFAccountUriRequest + */ class TwoFAccountUriRequestTest extends TestCase { use WithoutMiddleware; @@ -18,8 +21,8 @@ class TwoFAccountUriRequestTest extends TestCase public function test_user_is_authorized() { Auth::shouldReceive('check') - ->once() - ->andReturn(true); + ->once() + ->andReturn(true); $request = new TwoFAccountUriRequest(); @@ -29,7 +32,7 @@ public function test_user_is_authorized() /** * @dataProvider provideValidData */ - public function test_valid_data(array $data) : void + public function test_valid_data(array $data): void { $request = new TwoFAccountUriRequest(); $validator = Validator::make($data, $request->rules()); @@ -40,7 +43,7 @@ public function test_valid_data(array $data) : void /** * Provide Valid data for validation test */ - public function provideValidData() : array + public function provideValidData(): array { return [ [[ @@ -59,7 +62,7 @@ public function provideValidData() : array /** * @dataProvider provideInvalidData */ - public function test_invalid_data(array $data) : void + public function test_invalid_data(array $data): void { $request = new TwoFAccountUriRequest(); $validator = Validator::make($data, $request->rules()); @@ -70,7 +73,7 @@ public function test_invalid_data(array $data) : void /** * Provide invalid data for validation test */ - public function provideInvalidData() : array + public function provideInvalidData(): array { return [ [[ diff --git a/tests/Classes/LocalFileFactory.php b/tests/Classes/LocalFileFactory.php index 53864e39..fc257b9c 100644 --- a/tests/Classes/LocalFileFactory.php +++ b/tests/Classes/LocalFileFactory.php @@ -3,6 +3,8 @@ namespace Tests\Classes; use Illuminate\Http\Testing\File; +use Tests\Data\MigrationTestData; +use Tests\Data\OtpTestData; class LocalFileFactory { @@ -58,7 +60,7 @@ public function validAegisJsonFile() return new File('validAegisMigration.json', tap(tmpfile(), function ($temp) { ob_start(); - echo OtpTestData::AEGIS_JSON_MIGRATION_PAYLOAD; + echo MigrationTestData::VALID_AEGIS_JSON_MIGRATION_PAYLOAD; fwrite($temp, ob_get_clean()); })); @@ -74,7 +76,7 @@ public function invalidAegisJsonFile() return new File('invalidAegisMigration.json', tap(tmpfile(), function ($temp) { ob_start(); - echo OtpTestData::INVALID_AEGIS_JSON_MIGRATION_PAYLOAD; + echo MigrationTestData::INVALID_AEGIS_JSON_MIGRATION_PAYLOAD; fwrite($temp, ob_get_clean()); })); @@ -90,7 +92,7 @@ public function encryptedAegisJsonFile() return new File('encryptedAegisJsonFile.txt', tap(tmpfile(), function ($temp) { ob_start(); - echo OtpTestData::ENCRYPTED_AEGIS_JSON_MIGRATION_PAYLOAD; + echo MigrationTestData::ENCRYPTED_AEGIS_JSON_MIGRATION_PAYLOAD; fwrite($temp, ob_get_clean()); })); diff --git a/tests/Classes/OtpTestData.php b/tests/Classes/OtpTestData.php deleted file mode 100644 index 04e3e80d..00000000 --- a/tests/Classes/OtpTestData.php +++ /dev/null @@ -1,257 +0,0 @@ - self::SERVICE, - 'account' => self::ACCOUNT, - 'icon' => self::ICON, - 'otp_type' => 'totp', - 'secret' => self::SECRET, - 'digits' => self::DIGITS_CUSTOM, - 'algorithm' => self::ALGORITHM_CUSTOM, - 'period' => self::PERIOD_CUSTOM, - 'counter' => null, - ]; - - const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP = [ - 'account' => self::ACCOUNT, - 'otp_type' => 'totp', - 'secret' => self::SECRET, - ]; - - const ARRAY_OF_PARAMETERS_FOR_UNSUPPORTED_OTP_TYPE = [ - 'account' => self::ACCOUNT, - 'otp_type' => 'Xotp', - 'secret' => self::SECRET, - ]; - - const ARRAY_OF_INVALID_PARAMETERS_FOR_TOTP = [ - 'account' => self::ACCOUNT, - 'otp_type' => 'totp', - 'secret' => 0, - ]; - - const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP = [ - 'service' => self::SERVICE, - 'account' => self::ACCOUNT, - 'icon' => self::ICON, - 'otp_type' => 'hotp', - 'secret' => self::SECRET, - 'digits' => self::DIGITS_CUSTOM, - 'algorithm' => self::ALGORITHM_CUSTOM, - 'period' => null, - 'counter' => self::COUNTER_CUSTOM, - ]; - - const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP = [ - 'account' => self::ACCOUNT, - 'otp_type' => 'hotp', - 'secret' => self::SECRET, - ]; - - const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_STEAM_TOTP = [ - 'service' => self::STEAM, - 'account' => self::ACCOUNT, - 'otp_type' => 'steamtotp', - 'secret' => self::STEAM_SECRET, - 'digits' => self::DIGITS_STEAM, - 'algorithm' => self::ALGORITHM_DEFAULT, - 'period' => self::PERIOD_DEFAULT, - 'counter' => null, - ]; - - const GOOGLE_AUTH_MIGRATION_URI = 'otpauth-migration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY2UgASgBMAIKLAoKBw0SnrWITY/RFhILYWNjb3VudF9iaXMaC3NlcnZpY2VfYmlzIAEoATACEAEYASAA'; - - const INVALID_GOOGLE_AUTH_MIGRATION_URI = 'otpauthmigration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY2UgASgBMAIKLAoKBw0SnrWITY/RFhILYWNjb3VudF9iaXMaC3NlcnZpY2VfYmlzIAEoATACEAEYASAA'; - - const GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA = 'otpauth-migration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY'; - - const AEGIS_JSON_MIGRATION_PAYLOAD = ' - { - "version": 1, - "header": { - "slots": null, - "params": null - }, - "db": { - "version": 2, - "entries": [ - { - "type": "totp", - "uuid": "5be1b189-260d-4fe1-930a-a78cb669dd86", - "name": "' . self::ACCOUNT . '_totp", - "issuer": "' . self::SERVICE . '_totp", - "note": "", - "icon": null, - "info": { - "secret": "' . self::SECRET . '", - "algo": "' . self::ALGORITHM_DEFAULT . '", - "digits": ' . self::DIGITS_DEFAULT . ', - "period": ' . self::PERIOD_DEFAULT . ' - } - }, - { - "type": "totp", - "uuid": "fb2ebd05-9d71-4b2e-9d4e-b7f8d2942bfb", - "name": "' . self::ACCOUNT . '_totp_custom", - "issuer": "' . self::SERVICE . '_totp_custom", - "note": "", - "icon": null, - "info": { - "secret": "' . self::SECRET . '", - "algo": "' . self::ALGORITHM_CUSTOM . '", - "digits": ' . self::DIGITS_CUSTOM . ', - "period": ' . self::PERIOD_CUSTOM . ' - } - }, - { - "type": "hotp", - "uuid": "90a2af2e-2857-4515-bb18-52c4fa823f6f", - "name": "' . self::ACCOUNT . '_hotp", - "issuer": "' . self::SERVICE . '_hotp", - "note": "", - "icon": null, - "info": { - "secret": "' . self::SECRET . '", - "algo": "' . self::ALGORITHM_DEFAULT . '", - "digits": ' . self::DIGITS_DEFAULT . ', - "counter": ' . self::COUNTER_DEFAULT . ' - } - }, - { - "type": "hotp", - "uuid": "e1b3f683-d5fe-4126-b616-8c8abd8ad97c", - "name": "' . self::ACCOUNT . '_hotp_custom", - "issuer": "' . self::SERVICE . '_hotp_custom", - "note": "", - "icon": null, - "info": { - "secret": "' . self::SECRET . '", - "algo": "' . self::ALGORITHM_CUSTOM . '", - "digits": ' . self::DIGITS_CUSTOM . ', - "counter": ' . self::COUNTER_CUSTOM . ' - } - }, - { - "type": "steamtotp", - "uuid": "9fb06143-421d-46e1-a7e9-4aafe44b0e72", - "name": "' . self::ACCOUNT . '_steam", - "issuer": "' . self::STEAM . '", - "note": "", - "icon": "null", - "info": { - "secret": "' . self::STEAM_SECRET . '", - "algo": "' . self::ALGORITHM_DEFAULT . '", - "digits": ' . self::DIGITS_STEAM . ', - "period": ' . self::PERIOD_DEFAULT . ' - } - } - ] - } - }'; - - const INVALID_AEGIS_JSON_MIGRATION_PAYLOAD = ' - { - "version": 1, - "header": { - "slots": null, - "params": null - }, - "db": { - "version": 2, - "thisIsNotTheCorrectKeyName": [ - { - "type": "totp", - "uuid": "5be1b189-260d-4fe1-930a-a78cb669dd86", - "name": "' . self::ACCOUNT . '", - "issuer": "' . self::SERVICE . '", - "note": "", - "icon": null, - "info": { - "secret": "' . self::SECRET . '", - "algo": "' . self::ALGORITHM_DEFAULT . '", - "digits": ' . self::DIGITS_DEFAULT . ', - "period": ' . self::PERIOD_DEFAULT . ' - } - } - ] - } - }'; - - const ENCRYPTED_AEGIS_JSON_MIGRATION_PAYLOAD = ' - { - "version": 1, - "header": { - "slots": [ - { - "type": 1, - "uuid": "1f447956-c71c-4be4-8192-97197dc67df7", - "key": "d742967686cae462c5732023a72d59245d8q7c5c93a66aeb2q2a350bb8b6a7ae", - "key_params": { - "nonce": "77a8ff6d84265efd2a3ed9b7", - "tag": "cc13fb4a5baz3fd27bc97f5eacaa00d0" - }, - "n": 32768, - "r": 8, - "p": 1, - "salt": "1c245b2696b948dt040c30c538aeb6f9620b054d9ff182f33dd4b285b67bed51", - "repaired": true - } - ], - "params": { - "nonce": "f31675d9966d2z588bd07788", - "tag": "ad5729fa135dc6d6sw87e0c955932661" - } - }, - "db": "1rX0ajzsxNbhN2hvnNCMBNooLlzqwz\/LMT3bNEIJjPH+zIvIbA6GVVPHLpna+yvjxLPKVkt1OQig==" - }'; -} diff --git a/tests/Data/HttpRequestTestData.php b/tests/Data/HttpRequestTestData.php new file mode 100644 index 00000000..6ea1191b --- /dev/null +++ b/tests/Data/HttpRequestTestData.php @@ -0,0 +1,161 @@ +'; + + const TFA_JSON_BODY = ' + [ + [ + "Twitch", + { + "domain": "twitch.tv", + "url": "https://www.twitch.tv/", + "tfa": + [ + "sms", + "custom-software", + "totp" + ], + "custom-software": + [ + "Authy" + ], + "documentation": "https://help.twitch.tv/s/article/two-factor-authentication", + "notes": "To activate two factor authentication, you must provide a mobile phone number.", + "keywords": + [ + "entertainment" + ] + } + ], + [ + "Twitter", + { + "domain": "twitter.com", + "tfa": + [ + "sms", + "totp", + "u2f" + ], + "documentation": "https://help.twitter.com/en/managing-your-account/two-factor-authentication", + "recovery": "https://help.twitter.com/en/managing-your-account/issues-with-login-authentication", + "notes": "SMS only available on select providers.", + "keywords": + [ + "social" + ] + } + ], + [ + "Txbit", + { + "domain": "txbit.io", + "tfa": + [ + "totp" + ], + "documentation": "https://support.txbit.io/support/solutions/articles/44000447137", + "keywords": + [ + "cryptocurrencies" + ] + } + ] + ]'; + + const LATEST_RELEASE_BODY_NO_NEW_RELEASE = ' + { + "url": "https://api.github.com/repos/Bubka/2FAuth/releases/84186611", + "assets_url": "https://api.github.com/repos/Bubka/2FAuth/releases/84186611/assets", + "upload_url": "https://uploads.github.com/repos/Bubka/2FAuth/releases/84186611/assets{?name,label}", + "html_url": "https://github.com/Bubka/2FAuth/releases/tag/' . self::TAG_NAME . '", + "id": 84186611, + "author": { + "login": "Bubka", + "id": 858858, + "node_id": "MDQ6VXNlcjg1ODg1OA==", + "avatar_url": "https://avatars.githubusercontent.com/u/858858?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Bubka", + "html_url": "https://github.com/Bubka", + "followers_url": "https://api.github.com/users/Bubka/followers", + "following_url": "https://api.github.com/users/Bubka/following{/other_user}", + "gists_url": "https://api.github.com/users/Bubka/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Bubka/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Bubka/subscriptions", + "organizations_url": "https://api.github.com/users/Bubka/orgs", + "repos_url": "https://api.github.com/users/Bubka/repos", + "events_url": "https://api.github.com/users/Bubka/events{/privacy}", + "received_events_url": "https://api.github.com/users/Bubka/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "RE_kwDOCyNVx84FBJXz", + "tag_name": "' . self::TAG_NAME . '", + "target_commitish": "master", + "name": "' . self::TAG_NAME . '", + "draft": false, + "prerelease": false, + "created_at": "2022-11-25T13:31:45Z", + "published_at": "2022-11-25T13:44:10Z", + "assets": [ + + ], + "tarball_url": "https://api.github.com/repos/Bubka/2FAuth/tarball/' . self::TAG_NAME . '", + "zipball_url": "https://api.github.com/repos/Bubka/2FAuth/zipball/' . self::TAG_NAME . '", + "body": "### Fixed\r\n\r\n- [issue #140](https://github.com/Bubka/2FAuth/issues/140) Bad regex for Period field (advanced form)\r\n- [issue #141](https://github.com/Bubka/2FAuth/issues/141) Digits field is missing in advanced form" + }'; + + const LATEST_RELEASE_BODY_NEW_RELEASE = ' + { + "url": "https://api.github.com/repos/Bubka/2FAuth/releases/84186611", + "assets_url": "https://api.github.com/repos/Bubka/2FAuth/releases/84186611/assets", + "upload_url": "https://uploads.github.com/repos/Bubka/2FAuth/releases/84186611/assets{?name,label}", + "html_url": "https://github.com/Bubka/2FAuth/releases/tag/' . self::NEW_TAG_NAME . '", + "id": 84186611, + "author": { + "login": "Bubka", + "id": 858858, + "node_id": "MDQ6VXNlcjg1ODg1OA==", + "avatar_url": "https://avatars.githubusercontent.com/u/858858?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Bubka", + "html_url": "https://github.com/Bubka", + "followers_url": "https://api.github.com/users/Bubka/followers", + "following_url": "https://api.github.com/users/Bubka/following{/other_user}", + "gists_url": "https://api.github.com/users/Bubka/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Bubka/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Bubka/subscriptions", + "organizations_url": "https://api.github.com/users/Bubka/orgs", + "repos_url": "https://api.github.com/users/Bubka/repos", + "events_url": "https://api.github.com/users/Bubka/events{/privacy}", + "received_events_url": "https://api.github.com/users/Bubka/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "RE_kwDOCyNVx84FBJXz", + "tag_name": "' . self::NEW_TAG_NAME . '", + "target_commitish": "master", + "name": "' . self::NEW_TAG_NAME . '", + "draft": false, + "prerelease": false, + "created_at": "2022-12-25T13:31:45Z", + "published_at": "2022-12-25T13:44:10Z", + "assets": [ + + ], + "tarball_url": "https://api.github.com/repos/Bubka/2FAuth/tarball/' . self::NEW_TAG_NAME . '", + "zipball_url": "https://api.github.com/repos/Bubka/2FAuth/zipball/' . self::NEW_TAG_NAME . '", + "body": "### Fixed\r\n\r\n- [issue #140](https://github.com/Bubka/2FAuth/issues/140) Bad regex for Period field (advanced form)\r\n- [issue #141](https://github.com/Bubka/2FAuth/issues/141) Digits field is missing in advanced form" + }'; + + const ICON_PNG = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAsUlEQVR4AWN44aVBEhoCGl4GGLzND/nYW/Fpdsf7urTX8Q74NLwtjf7z+vl/VPDzwvFX4eYIDUhm6//99AGi6PfDOz9OH4Tr+TSrHYuG1/GOn+f3AtGnOV0vvLXeZPr8+/IJouHbthU4nJQfAtQANBuuFJ+GDx2F///9g6gAMn5dOfP34zt8Gr7tWQ838n1DBlDk973r+DS8Sff+snQKBL2KsQOKfJzSAOFC9EPQcEhLAD5LqIU3S31+AAAAAElFTkSuQmCC'; +} diff --git a/tests/Data/MigrationTestData.php b/tests/Data/MigrationTestData.php new file mode 100644 index 00000000..0b233f1a --- /dev/null +++ b/tests/Data/MigrationTestData.php @@ -0,0 +1,466 @@ + self::SERVICE, + 'account' => self::ACCOUNT, + 'icon' => self::ICON, + 'otp_type' => 'totp', + 'secret' => self::SECRET, + 'digits' => self::DIGITS_CUSTOM, + 'algorithm' => self::ALGORITHM_CUSTOM, + 'period' => self::PERIOD_CUSTOM, + 'counter' => null, + ]; + + const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP = [ + 'account' => self::ACCOUNT, + 'otp_type' => 'totp', + 'secret' => self::SECRET, + ]; + + const ARRAY_OF_PARAMETERS_FOR_UNSUPPORTED_OTP_TYPE = [ + 'account' => self::ACCOUNT, + 'otp_type' => 'Xotp', + 'secret' => self::SECRET, + ]; + + const ARRAY_OF_INVALID_PARAMETERS_FOR_TOTP = [ + 'account' => self::ACCOUNT, + 'otp_type' => 'totp', + 'secret' => 0, + ]; + + const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP = [ + 'service' => self::SERVICE, + 'account' => self::ACCOUNT, + 'icon' => self::ICON, + 'otp_type' => 'hotp', + 'secret' => self::SECRET, + 'digits' => self::DIGITS_CUSTOM, + 'algorithm' => self::ALGORITHM_CUSTOM, + 'period' => null, + 'counter' => self::COUNTER_CUSTOM, + ]; + + const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP = [ + 'account' => self::ACCOUNT, + 'otp_type' => 'hotp', + 'secret' => self::SECRET, + ]; + + const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_STEAM_TOTP = [ + 'service' => self::STEAM, + 'account' => self::ACCOUNT, + 'otp_type' => 'steamtotp', + 'secret' => self::STEAM_SECRET, + 'digits' => self::DIGITS_STEAM, + 'algorithm' => self::ALGORITHM_DEFAULT, + 'period' => self::PERIOD_DEFAULT, + 'counter' => null, + ]; + +} diff --git a/tests/Feature/Http/Auth/ForgotPasswordControllerTest.php b/tests/Feature/Http/Auth/ForgotPasswordControllerTest.php index bd939432..cf0ac678 100644 --- a/tests/Feature/Http/Auth/ForgotPasswordControllerTest.php +++ b/tests/Feature/Http/Auth/ForgotPasswordControllerTest.php @@ -9,6 +9,12 @@ use Illuminate\Support\Facades\Notification; use Tests\FeatureTestCase; +/** + * @covers \App\Http\Controllers\Auth\ForgotPasswordController + * @covers \App\Models\User + * @covers \App\Http\Middleware\RejectIfDemoMode + * @covers \App\Http\Middleware\RejectIfAuthenticated + */ class ForgotPasswordControllerTest extends FeatureTestCase { /** @@ -26,7 +32,7 @@ public function test_submit_email_password_request_without_email_returns_validat ]); $response->assertStatus(422) - ->assertJsonValidationErrors(['email']); + ->assertJsonValidationErrors(['email']); } /** @@ -39,7 +45,7 @@ public function test_submit_email_password_request_with_invalid_email_returns_va ]); $response->assertStatus(422) - ->assertJsonValidationErrors(['email']); + ->assertJsonValidationErrors(['email']); } /** @@ -52,7 +58,7 @@ public function test_submit_email_password_request_with_unknown_email_returns_va ]); $response->assertStatus(422) - ->assertJsonValidationErrors(['email']); + ->assertJsonValidationErrors(['email']); } /** @@ -91,4 +97,21 @@ public function test_submit_email_password_request_in_demo_mode_returns_unauthor $response->assertStatus(401); } + + /** + * @test + */ + public function test_submit_email_password_request_when_authenticated_returns_bad_request() + { + $user = User::factory()->create(); + + $this->actingAs($user, 'web-guard') + ->json('POST', '/user/password/lost', [ + 'email' => $user->email, + ]) + ->assertStatus(400) + ->assertJsonStructure([ + 'message', + ]); + } } diff --git a/tests/Feature/Http/Auth/LoginTest.php b/tests/Feature/Http/Auth/LoginTest.php index 2ad9fc9c..1f627cb5 100644 --- a/tests/Feature/Http/Auth/LoginTest.php +++ b/tests/Feature/Http/Auth/LoginTest.php @@ -5,7 +5,15 @@ use App\Facades\Settings; use App\Models\User; use Tests\FeatureTestCase; +use Illuminate\Support\Carbon; +/** + * @covers \App\Http\Controllers\Auth\LoginController + * @covers \App\Http\Middleware\RejectIfAuthenticated + * @covers \App\Http\Middleware\RejectIfReverseProxy + * @covers \App\Http\Middleware\RejectIfDemoMode + * @covers \App\Http\Middleware\SkipIfAuthenticated + */ class LoginTest extends FeatureTestCase { /** @@ -20,7 +28,7 @@ class LoginTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); @@ -36,15 +44,35 @@ public function test_user_login_returns_success() 'email' => $this->user->email, 'password' => self::PASSWORD, ]) - ->assertOk() - ->assertExactJson([ - 'message' => 'authenticated', - 'name' => $this->user->name, - ]); + ->assertOk() + ->assertExactJson([ + 'message' => 'authenticated', + 'name' => $this->user->name, + ]); } /** * @test + * + * @covers \App\Rules\CaseInsensitiveEmailExists + */ + public function test_user_login_with_uppercased_email_returns_success() + { + $response = $this->json('POST', '/user/login', [ + 'email' => strtoupper($this->user->email), + 'password' => self::PASSWORD, + ]) + ->assertOk() + ->assertExactJson([ + 'message' => 'authenticated', + 'name' => $this->user->name, + ]); + } + + /** + * @test + * + * @covers \App\Http\Middleware\SkipIfAuthenticated */ public function test_user_login_already_authenticated_returns_bad_request() { @@ -74,26 +102,28 @@ public function test_user_login_with_missing_data_returns_validation_error() 'email' => '', 'password' => '', ]) - ->assertStatus(422) - ->assertJsonValidationErrors([ - 'email', - 'password', - ]); + ->assertStatus(422) + ->assertJsonValidationErrors([ + 'email', + 'password', + ]); } /** * @test + * + * @covers \App\Exceptions\Handler */ - public function test_user_login_with_invalid_credentials_returns_validation_error() + public function test_user_login_with_invalid_credentials_returns_authentication_error() { $response = $this->json('POST', '/user/login', [ 'email' => $this->user->email, 'password' => self::WRONG_PASSWORD, ]) - ->assertStatus(401) - ->assertJson([ - 'message' => 'unauthorised', - ]); + ->assertStatus(401) + ->assertJson([ + 'message' => 'unauthorised', + ]); } /** @@ -154,6 +184,9 @@ public function test_user_logout_returns_validation_success() /** * @test + * + * @covers \App\Http\Middleware\KickOutInactiveUser + * @covers \App\Http\Middleware\LogUserLastSeen */ public function test_user_logout_after_inactivity_returns_teapot() { @@ -169,7 +202,7 @@ public function test_user_logout_after_inactivity_returns_teapot() $response = $this->actingAs($this->user, 'api-guard') ->json('GET', '/api/v1/twofaccounts'); - sleep(61); + $this->travelTo(Carbon::now()->addMinutes(2)); $response = $this->actingAs($this->user, 'api-guard') ->json('GET', '/api/v1/twofaccounts') diff --git a/tests/Feature/Http/Auth/PasswordControllerTest.php b/tests/Feature/Http/Auth/PasswordControllerTest.php index 76a30b34..ffcab462 100644 --- a/tests/Feature/Http/Auth/PasswordControllerTest.php +++ b/tests/Feature/Http/Auth/PasswordControllerTest.php @@ -5,6 +5,9 @@ use App\Models\User; use Tests\FeatureTestCase; +/** + * @covers \App\Http\Controllers\Auth\PasswordController + */ class PasswordControllerTest extends FeatureTestCase { /** @@ -19,7 +22,7 @@ class PasswordControllerTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); diff --git a/tests/Feature/Http/Auth/RegisterControllerTest.php b/tests/Feature/Http/Auth/RegisterControllerTest.php index aaf06623..b8cd4a34 100644 --- a/tests/Feature/Http/Auth/RegisterControllerTest.php +++ b/tests/Feature/Http/Auth/RegisterControllerTest.php @@ -6,6 +6,9 @@ use Illuminate\Support\Facades\DB; use Tests\FeatureTestCase; +/** + * @covers \App\Http\Controllers\Auth\RegisterController + */ class RegisterControllerTest extends FeatureTestCase { private const USERNAME = 'john doe'; @@ -17,7 +20,7 @@ class RegisterControllerTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); } @@ -35,18 +38,20 @@ public function test_register_returns_success() 'password' => self::PASSWORD, 'password_confirmation' => self::PASSWORD, ]) - ->assertCreated() - ->assertJsonStructure([ - 'message', - 'name', - ]) - ->assertJsonFragment([ - 'name' => self::USERNAME, - ]); + ->assertCreated() + ->assertJsonStructure([ + 'message', + 'name', + ]) + ->assertJsonFragment([ + 'name' => self::USERNAME, + ]); } /** * @test + * + * @covers \App\Rules\FirstUser */ public function test_register_returns_already_an_existing_user() { @@ -59,7 +64,7 @@ public function test_register_returns_already_an_existing_user() 'password' => self::PASSWORD, 'password_confirmation' => self::PASSWORD, ]) - ->assertJsonValidationErrorFor('name'); + ->assertJsonValidationErrorFor('name'); } /** diff --git a/tests/Feature/Http/Auth/ResetPasswordControllerTest.php b/tests/Feature/Http/Auth/ResetPasswordControllerTest.php index 583bdcb9..ed563009 100644 --- a/tests/Feature/Http/Auth/ResetPasswordControllerTest.php +++ b/tests/Feature/Http/Auth/ResetPasswordControllerTest.php @@ -8,6 +8,10 @@ use Illuminate\Support\Facades\Password; use Tests\FeatureTestCase; +/** + * @covers \App\Http\Controllers\Auth\ResetPasswordController + * @covers \App\Models\User + */ class ResetPasswordControllerTest extends FeatureTestCase { /** @@ -28,7 +32,7 @@ public function test_submit_reset_password_without_input_returns_validation_erro ]); $response->assertStatus(422) - ->assertJsonValidationErrors(['email', 'password', 'token']); + ->assertJsonValidationErrors(['email', 'password', 'token']); } /** @@ -44,7 +48,7 @@ public function test_submit_reset_password_with_invalid_data_returns_validation_ ]); $response->assertStatus(422) - ->assertJsonValidationErrors(['email', 'password']); + ->assertJsonValidationErrors(['email', 'password']); } /** @@ -60,7 +64,7 @@ public function test_submit_reset_password_with_too_short_pwd_returns_validation ]); $response->assertStatus(422) - ->assertJsonValidationErrors(['password']); + ->assertJsonValidationErrors(['password']); } /** diff --git a/tests/Feature/Http/Auth/UserControllerTest.php b/tests/Feature/Http/Auth/UserControllerTest.php index 153d62f5..2b24fa2e 100644 --- a/tests/Feature/Http/Auth/UserControllerTest.php +++ b/tests/Feature/Http/Auth/UserControllerTest.php @@ -7,6 +7,10 @@ use Illuminate\Support\Facades\Config; use Tests\FeatureTestCase; +/** + * @covers \App\Http\Controllers\Auth\UserController + * @covers \App\Http\Middleware\RejectIfDemoMode + */ class UserControllerTest extends FeatureTestCase { /** @@ -23,7 +27,7 @@ class UserControllerTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); diff --git a/tests/Feature/Http/Auth/WebAuthnDeviceLostControllerTest.php b/tests/Feature/Http/Auth/WebAuthnDeviceLostControllerTest.php index 5e7cd78d..0cef8d29 100644 --- a/tests/Feature/Http/Auth/WebAuthnDeviceLostControllerTest.php +++ b/tests/Feature/Http/Auth/WebAuthnDeviceLostControllerTest.php @@ -5,7 +5,16 @@ use App\Models\User; use Illuminate\Support\Facades\Notification; use Tests\FeatureTestCase; +use App\Notifications\WebauthnRecoveryNotification; +use Illuminate\Support\Facades\Lang; +/** + * @covers \App\Http\Controllers\Auth\WebAuthnDeviceLostController + * @covers \App\Notifications\WebauthnRecoveryNotification + * @covers \App\Extensions\WebauthnCredentialBroker + * @covers \App\Http\Requests\WebauthnDeviceLostRequest + * @covers \App\Providers\AuthServiceProvider + */ class WebAuthnDeviceLostControllerTest extends FeatureTestCase { /** @@ -16,7 +25,7 @@ class WebAuthnDeviceLostControllerTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); @@ -25,6 +34,7 @@ public function setUp() : void /** * @test + * @covers \App\Models\Traits\WebAuthnManageCredentials */ public function test_sendRecoveryEmail_sends_notification_on_success() { @@ -34,18 +44,60 @@ public function test_sendRecoveryEmail_sends_notification_on_success() 'email' => $this->user->email, ]); - Notification::assertSentTo($this->user, \App\Notifications\WebauthnRecoveryNotification::class); + Notification::assertSentTo($this->user, WebauthnRecoveryNotification::class); $response->assertStatus(200) - ->assertJsonStructure([ - 'message', + ->assertJsonStructure([ + 'message', + ]); + + $this->assertDatabaseHas('webauthn_recoveries', [ + 'email' => $this->user->email ]); } /** * @test */ - public function test_sendRecoveryEmail_does_not_send_anything_on_error() + public function test_WebauthnRecoveryNotification_renders_to_email() + { + $mail = (new WebauthnRecoveryNotification('test_token'))->toMail($this->user)->render(); + + $this->assertStringContainsString( + 'http://localhost/webauthn/recover?token=test_token&email=' . urlencode($this->user->email), + $mail + ); + + $this->assertStringContainsString( + Lang::get('Recover Account'), + $mail + ); + + $this->assertStringContainsString( + Lang::get( + 'You are receiving this email because we received an account recovery request for your account.' + ), + $mail + ); + + $this->assertStringContainsString( + Lang::get( + 'This recovery link will expire in :count minutes.', + ['count' => config('auth.passwords.webauthn.expire')] + ), + $mail + ); + + $this->assertStringContainsString( + Lang::get('If you did not request an account recovery, no further action is required.'), + $mail + ); + } + + /** + * @test + */ + public function test_sendRecoveryEmail_does_not_send_anything_to_unknown_email() { Notification::fake(); @@ -56,8 +108,103 @@ public function test_sendRecoveryEmail_does_not_send_anything_on_error() Notification::assertNothingSent(); $response->assertStatus(422) - ->assertJsonValidationErrors([ - 'email', + ->assertJsonValidationErrors([ + 'email', + ]); + + $this->assertDatabaseMissing('webauthn_recoveries', [ + 'email' => 'bad@email.com' ]); } + + /** + * @test + */ + public function test_sendRecoveryEmail_does_not_send_anything_to_invalid_email() + { + Notification::fake(); + + $response = $this->json('POST', '/webauthn/lost', [ + 'email' => 'bad@email.com', + ]); + + Notification::assertNothingSent(); + + $response->assertStatus(422) + ->assertJsonValidationErrors([ + 'email', + ]); + + $this->assertDatabaseMissing('webauthn_recoveries', [ + 'email' => 'bad@email.com' + ]); + } + + /** + * @test + */ + public function test_sendRecoveryEmail_does_not_send_anything_to_not_WebAuthnAuthenticatable() + { + $mock = $this->mock(\App\Extensions\WebauthnCredentialBroker::class)->makePartial(); + $mock->shouldReceive('getUser') + ->andReturn(new \Illuminate\Foundation\Auth\User()); + + Notification::fake(); + + $response = $this->json('POST', '/webauthn/lost', [ + 'email' => $this->user->email, + ]); + + Notification::assertNothingSent(); + + $response->assertStatus(422) + ->assertJsonValidationErrors([ + 'email', + ]); + } + + /** + * @test + */ + public function test_sendRecoveryEmail_is_throttled() + { + Notification::fake(); + + $response = $this->json('POST', '/webauthn/lost', [ + 'email' => $this->user->email, + ]); + + Notification::assertSentTo($this->user, WebauthnRecoveryNotification::class); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'message', + ]); + + $this->assertDatabaseHas('webauthn_recoveries', [ + 'email' => $this->user->email + ]); + + $this->json('POST', '/webauthn/lost', [ + 'email' => $this->user->email, + ]) + ->assertStatus(422) + ->assertJsonValidationErrorfor('email') + ->assertJsonFragment([ + 'message' => __('passwords.throttled') + ]); + } + + /** + * @test + */ + public function test_error_if_no_broker_is_set() + { + $this->app['config']->set('auth.passwords.webauthn', null); + + $this->json('POST', '/webauthn/lost', [ + 'email' => $this->user->email + ]) + ->assertStatus(500); + } } diff --git a/tests/Feature/Http/Auth/WebAuthnLoginControllerTest.php b/tests/Feature/Http/Auth/WebAuthnLoginControllerTest.php index 81c6b3c9..9348ea8d 100644 --- a/tests/Feature/Http/Auth/WebAuthnLoginControllerTest.php +++ b/tests/Feature/Http/Auth/WebAuthnLoginControllerTest.php @@ -6,7 +6,14 @@ use Illuminate\Support\Facades\DB; use Laragear\WebAuthn\Http\Requests\AssertedRequest; use Tests\FeatureTestCase; +use Laragear\WebAuthn\WebAuthn; +use Illuminate\Support\Facades\Config; +use Laragear\WebAuthn\Assertion\Validator\AssertionValidator; +/** + * @covers \App\Http\Controllers\Auth\WebAuthnLoginController + * @covers \App\Models\User + */ class WebAuthnLoginControllerTest extends FeatureTestCase { /** @@ -15,15 +22,44 @@ class WebAuthnLoginControllerTest extends FeatureTestCase protected $user; const CREDENTIAL_ID = 's06aG41wsIYh5X1YUhB-SlH8y3F2RzdJZVse8iXRXOCd3oqQdEyCOsBawzxrYBtJRQA2azAMEN_q19TUp6iMgg'; + const CREDENTIAL_ID_ALT = '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg'; + const CREDENTIAL_ID_ALT_RAW = '+VOLFKPY+/FuMI/sJ7gMllK76L3VoRUINj6lL/Z3qDg='; const PUBLIC_KEY = 'eyJpdiI6ImYyUHlJOEJML0pwTXJ2UDkveTQwZFE9PSIsInZhbHVlIjoiQWFSYi9LVEszazlBRUZsWHp0cGNRNktGeEQ3aTBsbU9zZ1g5MEgrWFJJNmgraElsNU9hV0VsRVlWc3NoUVVHUjRRdlcxTS9pVklnOWtVYWY5TFJQTTFhR1Rxb1ZzTFkxTWE4VUVvK1lyU3pYQ1M3VlBMWWxZcDVaYWFnK25iaXVyWGR6ZFRmMFVoSmdPZ3UvSnptbVZER0FYdEEyYmNYcW43RkV5aTVqSjNwZEFsUjhUYSs0YjU2Z2V2bUJXa0E0aVB1VC8xSjdJZ2llRGlHY2RwOGk3MmNPTyt6eDFDWUs1dVBOSWp1ZUFSeUlkclgwRW16RE9sUUpDSWV6Sk50TSIsIm1hYyI6IjI3ODQ5NzcxZGY1MzMwYTNiZjAwZmEwMDJkZjYzMGU4N2UzZjZlOGM0ZWE3NDkyYWMxMThhNmE5NWZiMTVjNGEiLCJ0YWciOiIifQ=='; const USER_ID = '3b758ac868b74307a7e96e69ae187339'; + const USER_ID_ALT = 'e8af6f703f8042aa91c30cf72289aa07'; + + const ASSERTION_RESPONSE = [ + 'id' => self::CREDENTIAL_ID_ALT, + 'rawId' => self::CREDENTIAL_ID_ALT_RAW, + 'type' => 'public-key', + 'response' => [ + 'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9', + 'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==', + 'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==', + 'userHandle' => self::USER_ID_ALT, + ] + ]; + + const ASSERTION_RESPONSE_NO_HANDLE = [ + 'id' => self::CREDENTIAL_ID_ALT, + 'rawId' => self::CREDENTIAL_ID_ALT_RAW, + 'type' => 'public-key', + 'response' => [ + 'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9', + 'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==', + 'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==', + 'userHandle' => null, + ] + ]; + + const ASSERTION_CHALLENGE = 'iXozmynKi+YD2iRvKNbSPA=='; /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); @@ -47,6 +83,42 @@ public function test_webauthn_login_uses_login_and_returns_no_content() ->assertNoContent(); } + /** + * @test + */ + public function test_webauthn_login_merge_handle_if_missing() + { + $this->user = User::factory()->create(); + + DB::table('webauthn_credentials')->insert([ + 'id' => self::CREDENTIAL_ID_ALT, + 'authenticatable_type' => \App\Models\User::class, + 'authenticatable_id' => $this->user->id, + 'user_id' => self::USER_ID_ALT, + 'counter' => 0, + 'rp_id' => 'http://localhost', + 'origin' => 'http://localhost', + 'aaguid' => '00000000-0000-0000-0000-000000000000', + 'attestation_format' => 'none', + 'public_key' => self::PUBLIC_KEY, + 'updated_at' => now(), + 'created_at' => now(), + ]); + + $this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge( + new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)), + 60, + false, + )]); + + $this->mock(AssertionValidator::class) + ->expects('send->thenReturn') + ->andReturn(); + + $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_NO_HANDLE) + ->assertNoContent(); + } + /** * @test */ @@ -84,22 +156,24 @@ public function test_webauthn_login_with_missing_data_returns_validation_error() ]; $response = $this->json('POST', '/webauthn/login', $data) - ->assertStatus(422) - ->assertJsonValidationErrors([ - 'id', - 'rawId', - 'type', - 'response.authenticatorData', - 'response.clientDataJSON', - 'response.signature', - ]); + ->assertStatus(422) + ->assertJsonValidationErrors([ + 'id', + 'rawId', + 'type', + 'response.authenticatorData', + 'response.clientDataJSON', + 'response.signature', + ]); } /** * @test */ - public function test_get_options_returns_success() + public function test_get_options_for_securelogin_returns_success() { + Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_REQUIRED); + $this->user = User::factory()->create(); DB::table('webauthn_credentials')->insert([ @@ -118,18 +192,59 @@ public function test_get_options_returns_success() ]); $response = $this->json('POST', '/webauthn/login/options') - ->assertOk() - ->assertJsonStructure([ - 'challenge', - 'userVerification', - 'timeout', - ]) - ->assertJsonFragment([ - 'allowCredentials' => [[ - 'id' => self::CREDENTIAL_ID, - 'type' => 'public-key', - ]], + ->assertOk() + ->assertJsonStructure([ + 'challenge', + 'userVerification', + 'timeout', + ]) + ->assertJsonFragment([ + 'userVerification' => 'required', + 'allowCredentials' => [[ + 'id' => self::CREDENTIAL_ID, + 'type' => 'public-key', + ]], + ]); + } + + /** + * @test + */ + public function test_get_options_for_fastlogin_returns_success() + { + Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_DISCOURAGED); + + $this->user = User::factory()->create(); + + DB::table('webauthn_credentials')->insert([ + 'id' => self::CREDENTIAL_ID, + 'authenticatable_type' => \App\Models\User::class, + 'authenticatable_id' => $this->user->id, + 'user_id' => self::USER_ID, + 'counter' => 0, + 'rp_id' => 'http://localhost', + 'origin' => 'http://localhost', + 'aaguid' => '00000000-0000-0000-0000-000000000000', + 'attestation_format' => 'none', + 'public_key' => self::PUBLIC_KEY, + 'updated_at' => now(), + 'created_at' => now(), ]); + + $response = $this->json('POST', '/webauthn/login/options') + ->assertOk() + ->assertJsonStructure([ + 'challenge', + 'userVerification', + 'timeout', + ]) + ->assertJsonFragment([ + 'userVerification' => 'discouraged', + 'allowCredentials' => [[ + 'id' => self::CREDENTIAL_ID, + 'type' => 'public-key', + ]], + ]); } /** @@ -138,9 +253,9 @@ public function test_get_options_returns_success() public function test_get_options_with_no_registred_user_returns_error() { $this->json('POST', '/webauthn/login/options') - ->assertStatus(400) - ->assertJsonStructure([ - 'message', - ]); + ->assertStatus(400) + ->assertJsonStructure([ + 'message', + ]); } } diff --git a/tests/Feature/Http/Auth/WebAuthnManageControllerTest.php b/tests/Feature/Http/Auth/WebAuthnManageControllerTest.php index 4fc735f2..d3678cb6 100644 --- a/tests/Feature/Http/Auth/WebAuthnManageControllerTest.php +++ b/tests/Feature/Http/Auth/WebAuthnManageControllerTest.php @@ -7,6 +7,11 @@ use Illuminate\Support\Facades\DB; use Tests\FeatureTestCase; +/** + * @covers \App\Http\Controllers\Auth\WebAuthnManageController + * @covers \App\Http\Middleware\RejectIfReverseProxy + * @covers \App\Models\Traits\WebAuthnManageCredentials + */ class WebAuthnManageControllerTest extends FeatureTestCase { // use WithoutMiddleware; @@ -23,7 +28,7 @@ class WebAuthnManageControllerTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); diff --git a/tests/Feature/Http/Auth/WebAuthnRecoveryControllerTest.php b/tests/Feature/Http/Auth/WebAuthnRecoveryControllerTest.php index e8ab62b6..191f30d7 100644 --- a/tests/Feature/Http/Auth/WebAuthnRecoveryControllerTest.php +++ b/tests/Feature/Http/Auth/WebAuthnRecoveryControllerTest.php @@ -8,6 +8,12 @@ use Illuminate\Support\Facades\DB; use Tests\FeatureTestCase; +/** + * @covers \App\Http\Controllers\Auth\WebAuthnRecoveryController + * @covers \App\Extensions\WebauthnCredentialBroker + * @covers \App\Http\Requests\WebauthnRecoveryRequest + * @covers \App\Providers\AuthServiceProvider + */ class WebAuthnRecoveryControllerTest extends FeatureTestCase { /** @@ -29,7 +35,7 @@ class WebAuthnRecoveryControllerTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); @@ -47,16 +53,55 @@ public function setUp() : void /** * @test */ - public function test_recover_with_invalid_token_returns_validation_error() + public function test_recover_fails_if_no_recovery_is_set() { - $response = $this->json('POST', '/webauthn/recover', [ - 'token' => 'bad_token', + DB::table('webauthn_recoveries')->delete(); + + $this->json('POST', '/webauthn/recover', [ + 'token' => self::ACTUAL_TOKEN_VALUE, 'email' => $this->user->email, 'password' => UserFactory::USER_PASSWORD, ]) - ->assertStatus(422) - ->assertJsonMissingValidationErrors('email') - ->assertJsonValidationErrors('token'); + ->assertStatus(422) + ->assertJsonValidationErrors('token'); + } + + /** + * @test + */ + public function test_recover_with_wrong_token_returns_validation_error() + { + $response = $this->json('POST', '/webauthn/recover', [ + 'token' => 'wrong_token', + 'email' => $this->user->email, + 'password' => UserFactory::USER_PASSWORD, + ]) + ->assertStatus(422) + ->assertJsonMissingValidationErrors('email') + ->assertJsonValidationErrors('token'); + } + + /** + * @test + */ + public function test_recover_with_expired_token_returns_validation_error() + { + Date::setTestNow($now = Date::create(2020, 01, 01, 16, 30)); + + DB::table('webauthn_recoveries')->delete(); + DB::table('webauthn_recoveries')->insert([ + 'token' => self::STORED_TOKEN_VALUE, + 'email' => $this->user->email, + 'created_at' => $now->clone()->subHour()->subSecond()->toDateTimeString(), + ]); + + $this->json('POST', '/webauthn/recover', [ + 'token' => self::ACTUAL_TOKEN_VALUE, + 'email' => $this->user->email, + 'password' => UserFactory::USER_PASSWORD, + ]) + ->assertStatus(422) + ->assertJsonValidationErrors('token'); } /** @@ -64,12 +109,28 @@ public function test_recover_with_invalid_token_returns_validation_error() */ public function test_recover_with_invalid_password_returns_authentication_error() { - $response = $this->json('POST', '/webauthn/recover', [ + $this->json('POST', '/webauthn/recover', [ 'token' => self::ACTUAL_TOKEN_VALUE, 'email' => $this->user->email, 'password' => 'bad_password', ]) - ->assertStatus(401); + ->assertStatus(401); + } + + /** + * @test + */ + public function test_recover_returns_validation_error_when_no_user_exists() + { + $this->json('POST', '/webauthn/recover', [ + 'token' => self::ACTUAL_TOKEN_VALUE, + 'email' => 'no@user.com', + 'password' => UserFactory::USER_PASSWORD, + ]) + ->assertStatus(422) + ->assertJsonMissingValidationErrors('password') + ->assertJsonMissingValidationErrors('token') + ->assertJsonValidationErrors('email'); } /** @@ -82,7 +143,7 @@ public function test_recover_returns_success() 'email' => $this->user->email, 'password' => UserFactory::USER_PASSWORD, ]) - ->assertStatus(200); + ->assertStatus(200); $this->assertDatabaseMissing('webauthn_recoveries', [ 'token' => self::STORED_TOKEN_VALUE, @@ -119,7 +180,7 @@ public function test_revoke_all_credentials_clear_registered_credentials() 'password' => UserFactory::USER_PASSWORD, 'revokeAll' => true, ]) - ->assertStatus(200); + ->assertStatus(200); $this->assertDatabaseMissing('webauthn_credentials', [ 'authenticatable_id' => $this->user->id, diff --git a/tests/Feature/Http/Auth/WebAuthnRegisterControllerTest.php b/tests/Feature/Http/Auth/WebAuthnRegisterControllerTest.php new file mode 100644 index 00000000..01927c34 --- /dev/null +++ b/tests/Feature/Http/Auth/WebAuthnRegisterControllerTest.php @@ -0,0 +1,79 @@ +user = User::factory()->create(); + } + + /** + * @test + */ + public function test_uses_attestation_with_fastRegistration_request(): void + { + Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_DISCOURAGED); + + $request = $this->mock(AttestationRequest::class); + + $request->expects('fastRegistration')->andReturnSelf(); + $request->expects('toCreate')->andReturn(new JsonTransport()); + + $this->actingAs($this->user, 'web-guard') + ->json('POST', '/webauthn/register/options') + ->assertOk(); + } + + /** + * @test + */ + public function test_uses_attestation_with_secureRegistration_request(): void + { + Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_REQUIRED); + + $request = $this->mock(AttestationRequest::class); + + $request->expects('secureRegistration')->andReturnSelf(); + $request->expects('toCreate')->andReturn(new JsonTransport()); + + $this->actingAs($this->user, 'web-guard') + ->json('POST', '/webauthn/register/options') + ->assertOk(); + } + + /** + * @test + */ + public function test_register_uses_attested_request(): void + { + $this->mock(AttestedRequest::class)->expects('save')->andReturn(); + + $this->actingAs($this->user, 'web-guard') + ->json('POST', '/webauthn/register') + ->assertNoContent(); + } +} diff --git a/tests/Feature/Http/SystemControllerTest.php b/tests/Feature/Http/SystemControllerTest.php new file mode 100644 index 00000000..83132910 --- /dev/null +++ b/tests/Feature/Http/SystemControllerTest.php @@ -0,0 +1,122 @@ +user = User::factory()->create(); + } + + /** + * @test + */ + public function test_infos_returns_only_base_collection() + { + $response = $this->json('GET', '/infos') + ->assertOk() + ->assertJsonStructure([ + 'Date', + 'userAgent', + 'Version', + 'Environment', + 'Debug', + 'Cache driver', + 'Log channel', + 'Log level', + 'DB driver', + 'PHP version', + 'Operating system', + 'interface', + ]); + } + + /** + * @test + */ + public function test_infos_returns_full_collection_when_signed_in() + { + $response = $this->actingAs($this->user, 'api-guard') + ->json('GET', '/infos') + ->assertOk() + ->assertJsonStructure([ + 'Auth guard', + 'webauthn user verification', + 'Trusted proxies', + 'options' => [ + 'showTokenAsDot', + 'closeOtpOnCopy', + 'copyOtpOnDisplay', + 'useBasicQrcodeReader', + 'displayMode', + 'showAccountsIcons', + 'kickUserAfter', + 'activeGroup', + 'rememberActiveGroup', + 'defaultGroup', + 'useEncryption', + 'defaultCaptureMode', + 'useDirectCapture', + 'useWebauthnAsDefault', + 'useWebauthnOnly', + 'getOfficialIcons', + 'checkForUpdate', + 'lastRadarScan', + 'latestRelease', + 'lang', + ], + ]); + } + + /** + * @test + */ + public function test_infos_returns_full_collection_when_signed_in_behind_proxy() + { + $response = $this->actingAs($this->user, 'reverse-proxy-guard') + ->json('GET', '/infos') + ->assertOk() + ->assertJsonStructure([ + 'Auth proxy header for user', + 'Auth proxy header for email', + ]); + } + + /** + * @test + */ + public function test_latestrelease_runs_manual_scan() + { + $releaseRadarService = $this->mock(ReleaseRadarService::class)->makePartial(); + $releaseRadarService->shouldReceive('manualScan') + ->once() + ->andReturn('new_release'); + + $response = $this->json('GET', '/latestRelease') + ->assertOk() + ->assertJson([ + 'newRelease' => 'new_release', + ]); + } +} diff --git a/tests/Feature/Models/TwoFAccountModelTest.php b/tests/Feature/Models/TwoFAccountModelTest.php index b111c8b8..f1b078e8 100644 --- a/tests/Feature/Models/TwoFAccountModelTest.php +++ b/tests/Feature/Models/TwoFAccountModelTest.php @@ -3,8 +3,14 @@ namespace Tests\Feature\Models; use App\Models\TwoFAccount; -use Tests\Classes\OtpTestData; +use Tests\Data\OtpTestData; use Tests\FeatureTestCase; +use Illuminate\Support\Facades\Storage; +use Illuminate\Http\Testing\FileFactory; +use Illuminate\Support\Facades\Http; +use App\Helpers\Helpers; +use Mockery\MockInterface; +use Tests\Data\HttpRequestTestData; /** * @covers \App\Models\TwoFAccount @@ -21,10 +27,15 @@ class TwoFAccountModelTest extends FeatureTestCase */ protected $customHotpTwofaccount; + /** + * + */ + const ICON_NAME = 'oDBngpjQaQAgLtHqGuYiPRqftCXv6Sj4hSAXARpA.png'; + /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); @@ -69,12 +80,36 @@ public function setUp() : void /** * @test + * + * @runInSeparateProcess + * @preserveGlobalState disabled */ public function test_fill_with_custom_totp_uri_returns_correct_value() { + $this->mock('alias:' . Helpers::class, function (MockInterface $helper) { + $helper->shouldReceive('getUniqueFilename') + ->andReturn(self::ICON_NAME); + + $helper->shouldReceive('isValidImage') + ->andReturn(true); + }); + + $file = (new FileFactory)->image(self::ICON_NAME, 10, 10); + + Http::preventStrayRequests(); + Http::fake([ + 'https://en.opensuse.org/images/4/44/Button-filled-colour.png' => Http::response($file->tempFile, 200), + ]); + + Storage::fake('imagesLink'); + Storage::fake('icons'); + $twofaccount = new TwoFAccount; $twofaccount->fillWithURI(OtpTestData::TOTP_FULL_CUSTOM_URI); + Storage::disk('icons')->assertExists(self::ICON_NAME); + Storage::disk('imagesLink')->assertMissing(self::ICON_NAME); + $this->assertEquals('totp', $twofaccount->otp_type); $this->assertEquals(OtpTestData::TOTP_FULL_CUSTOM_URI, $twofaccount->legacy_uri); $this->assertEquals(OtpTestData::SERVICE, $twofaccount->service); @@ -84,7 +119,7 @@ public function test_fill_with_custom_totp_uri_returns_correct_value() $this->assertEquals(OtpTestData::PERIOD_CUSTOM, $twofaccount->period); $this->assertEquals(null, $twofaccount->counter); $this->assertEquals(OtpTestData::ALGORITHM_CUSTOM, $twofaccount->algorithm); - $this->assertStringEndsWith('.png', $twofaccount->icon); + $this->assertEquals(self::ICON_NAME, $twofaccount->icon); } /** @@ -109,12 +144,36 @@ public function test_fill_with_basic_totp_uri_returns_default_value() /** * @test + * + * @runInSeparateProcess + * @preserveGlobalState disabled */ public function test_fill_with_custom_hotp_uri_returns_correct_value() { + $this->mock('alias:' . Helpers::class, function (MockInterface $helper) { + $helper->shouldReceive('getUniqueFilename') + ->andReturn(self::ICON_NAME); + + $helper->shouldReceive('isValidImage') + ->andReturn(true); + }); + + $file = (new FileFactory)->image(self::ICON_NAME, 10, 10); + + Http::preventStrayRequests(); + Http::fake([ + 'https://en.opensuse.org/images/4/44/Button-filled-colour.png' => Http::response($file->tempFile, 200), + ]); + + Storage::fake('imagesLink'); + Storage::fake('icons'); + $twofaccount = new TwoFAccount; $twofaccount->fillWithURI(OtpTestData::HOTP_FULL_CUSTOM_URI); + Storage::disk('icons')->assertExists(self::ICON_NAME); + Storage::disk('imagesLink')->assertMissing(self::ICON_NAME); + $this->assertEquals('hotp', $twofaccount->otp_type); $this->assertEquals(OtpTestData::HOTP_FULL_CUSTOM_URI, $twofaccount->legacy_uri); $this->assertEquals(OtpTestData::SERVICE, $twofaccount->service); @@ -124,7 +183,7 @@ public function test_fill_with_custom_hotp_uri_returns_correct_value() $this->assertEquals(null, $twofaccount->period); $this->assertEquals(OtpTestData::COUNTER_CUSTOM, $twofaccount->counter); $this->assertEquals(OtpTestData::ALGORITHM_CUSTOM, $twofaccount->algorithm); - $this->assertStringEndsWith('.png', $twofaccount->icon); + $this->assertEquals(self::ICON_NAME, $twofaccount->icon); } /** @@ -391,9 +450,28 @@ public function test_update_totp_persists_updated_model() /** * @test + * + * @runInSeparateProcess + * @preserveGlobalState disabled */ public function test_getOTP_for_totp_returns_the_same_password() { + $this->mock('alias:' . Helpers::class, function (MockInterface $helper) { + $helper->shouldReceive('getUniqueFilename') + ->andReturn(self::ICON_NAME); + + $helper->shouldReceive('isValidImage') + ->andReturn(true); + }); + + Http::preventStrayRequests(); + Http::fake([ + 'https://en.opensuse.org/images/4/44/Button-filled-colour.png' => Http::response(HttpRequestTestData::ICON_PNG, 200), + ]); + + Storage::fake('imagesLink'); + Storage::fake('icons'); + $twofaccount = new TwoFAccount; $otp_from_model = $this->customTotpTwofaccount->getOTP(); @@ -413,9 +491,28 @@ public function test_getOTP_for_totp_returns_the_same_password() /** * @test + * + * @runInSeparateProcess + * @preserveGlobalState disabled */ public function test_getOTP_for_hotp_returns_the_same_password() { + $this->mock('alias:' . Helpers::class, function (MockInterface $helper) { + $helper->shouldReceive('getUniqueFilename') + ->andReturn(self::ICON_NAME); + + $helper->shouldReceive('isValidImage') + ->andReturn(true); + }); + + Http::preventStrayRequests(); + Http::fake([ + 'https://en.opensuse.org/images/4/44/Button-filled-colour.png' => Http::response(HttpRequestTestData::ICON_PNG, 200), + ]); + + Storage::fake('imagesLink'); + Storage::fake('icons'); + $twofaccount = new TwoFAccount; $otp_from_model = $this->customHotpTwofaccount->getOTP(); @@ -507,4 +604,107 @@ public function test_getURI_for_custom_hotp_model_returns_uri() $this->assertStringContainsString('counter=' . OtpTestData::COUNTER_CUSTOM, $uri); $this->assertStringContainsString('algorithm=' . OtpTestData::ALGORITHM_CUSTOM, $uri); } + + /** + * @test + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_fill_succeed_when_image_fetching_fails() + { + $this->mock('alias:' . Helpers::class, function (MockInterface $helper) { + $helper->shouldReceive('getUniqueFilename') + ->andReturn(self::ICON_NAME); + }); + + Http::preventStrayRequests(); + + Storage::fake('imagesLink'); + Storage::fake('icons'); + + $twofaccount = new TwoFAccount; + $twofaccount->fillWithURI(OtpTestData::TOTP_FULL_CUSTOM_URI); + + Storage::disk('icons')->assertMissing(self::ICON_NAME); + Storage::disk('imagesLink')->assertMissing(self::ICON_NAME); + } + + /** + * @test + */ + public function test_saving_totp_without_period_set_default_one() + { + $twofaccount = new TwoFAccount; + $twofaccount->service = OtpTestData::SERVICE; + $twofaccount->account = OtpTestData::ACCOUNT; + $twofaccount->otp_type = TwoFAccount::TOTP; + $twofaccount->secret = OtpTestData::SECRET; + + $twofaccount->save(); + + $account = TwoFAccount::find($twofaccount->id); + + $this->assertEquals(TwoFAccount::DEFAULT_PERIOD, $account->period); + } + + /** + * @test + */ + public function test_saving_hotp_without_counter_set_default_one() + { + $twofaccount = new TwoFAccount; + $twofaccount->service = OtpTestData::SERVICE; + $twofaccount->account = OtpTestData::ACCOUNT; + $twofaccount->otp_type = TwoFAccount::HOTP; + $twofaccount->secret = OtpTestData::SECRET; + + $twofaccount->save(); + + $account = TwoFAccount::find($twofaccount->id); + + $this->assertEquals(TwoFAccount::DEFAULT_COUNTER, $account->counter); + } + + /** + * @test + */ + public function test_equals_returns_true() + { + $twofaccount = new TwoFAccount; + $twofaccount->legacy_uri = OtpTestData::TOTP_FULL_CUSTOM_URI; + $twofaccount->service = OtpTestData::SERVICE; + $twofaccount->account = OtpTestData::ACCOUNT; + $twofaccount->icon = OtpTestData::ICON; + $twofaccount->otp_type = 'totp'; + $twofaccount->secret = OtpTestData::SECRET; + $twofaccount->digits = OtpTestData::DIGITS_CUSTOM; + $twofaccount->algorithm = OtpTestData::ALGORITHM_CUSTOM; + $twofaccount->period = OtpTestData::PERIOD_CUSTOM; + $twofaccount->counter = null; + $twofaccount->save(); + + $this->assertTrue($twofaccount->equals($this->customTotpTwofaccount)); + } + + /** + * @test + */ + public function test_equals_returns_false() + { + $twofaccount = new TwoFAccount; + $twofaccount->legacy_uri = OtpTestData::TOTP_FULL_CUSTOM_URI; + $twofaccount->service = OtpTestData::SERVICE; + $twofaccount->account = OtpTestData::ACCOUNT; + $twofaccount->icon = OtpTestData::ICON; + $twofaccount->otp_type = 'totp'; + $twofaccount->secret = OtpTestData::SECRET; + $twofaccount->digits = OtpTestData::DIGITS_CUSTOM; + $twofaccount->algorithm = OtpTestData::ALGORITHM_CUSTOM; + $twofaccount->period = OtpTestData::PERIOD_CUSTOM; + $twofaccount->counter = null; + $twofaccount->save(); + + $this->assertFalse($twofaccount->equals($this->customHotpTwofaccount)); + } } diff --git a/tests/Feature/Services/GroupServiceTest.php b/tests/Feature/Services/GroupServiceTest.php index a264b223..ccf1a271 100644 --- a/tests/Feature/Services/GroupServiceTest.php +++ b/tests/Feature/Services/GroupServiceTest.php @@ -10,6 +10,7 @@ /** * @covers \App\Services\GroupService + * @covers \App\Facades\Groups */ class GroupServiceTest extends FeatureTestCase { @@ -52,7 +53,7 @@ class GroupServiceTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); diff --git a/tests/Feature/Services/LogoServiceTest.php b/tests/Feature/Services/LogoServiceTest.php index 4a846c78..fadc7fe5 100644 --- a/tests/Feature/Services/LogoServiceTest.php +++ b/tests/Feature/Services/LogoServiceTest.php @@ -6,6 +6,9 @@ use Illuminate\Foundation\Testing\WithoutMiddleware; use Mockery\MockInterface; use Tests\TestCase; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Http; +use Tests\Data\HttpRequestTestData; /** * @covers \App\Services\LogoService @@ -17,7 +20,7 @@ class LogoServiceTest extends TestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); } @@ -25,18 +28,65 @@ public function setUp() : void /** * @test */ - public function test_getIcon_returns_iconFilename_when_logo_exists() + public function test_getIcon_returns_stored_icon_file_when_logo_exists() { - $logoServiceMock = $this->partialMock(LogoService::class, function (MockInterface $mock) { - $mock->shouldAllowMockingProtectedMethods(); - $mock->shouldReceive('getLogo', 'copyToIcons') - ->once() - ->andReturn('service.svg', true); - }); + $svgLogo = HttpRequestTestData::SVG_LOGO_BODY; + $tfaJsonBody = HttpRequestTestData::TFA_JSON_BODY; - $icon = $logoServiceMock->getIcon('service'); + Http::preventStrayRequests(); + Http::fake([ + 'https://raw.githubusercontent.com/2factorauth/twofactorauth/master/img/*' => Http::response($svgLogo, 200), + 'https://2fa.directory/api/v3/tfa.json' => Http::response($tfaJsonBody, 200), + ]); + + Storage::fake('icons'); + Storage::fake('logos'); + + $logoService = new LogoService(); + $icon = $logoService->getIcon('twitter'); $this->assertNotNull($icon); + Storage::disk('icons')->assertExists($icon); + } + + /** + * @test + */ + public function test_getIcon_returns_null_when_github_request_fails() + { + Http::preventStrayRequests(); + Http::fake([ + 'https://raw.githubusercontent.com/2factorauth/twofactorauth/master/img/*' => Http::response('not found', 404), + ]); + + Storage::fake('icons'); + Storage::fake('logos'); + $logoService = new LogoService(); + + $icon = $logoService->getIcon('twitter'); + + $this->assertEquals(null, $icon); + } + + /** + * @test + */ + public function test_getIcon_returns_null_when_logo_fetching_fails() + { + $tfaJsonBody = HttpRequestTestData::TFA_JSON_BODY; + + Http::preventStrayRequests(); + Http::fake([ + 'https://2fa.directory/api/v3/tfa.json' => Http::response($tfaJsonBody, 200), + ]); + + Storage::fake('icons'); + Storage::fake('logos'); + $logoService = new LogoService(); + + $icon = $logoService->getIcon('twitter'); + + $this->assertEquals(null, $icon); } /** @@ -44,15 +94,32 @@ public function test_getIcon_returns_iconFilename_when_logo_exists() */ public function test_getIcon_returns_null_when_no_logo_exists() { - $logoServiceMock = $this->partialMock(LogoService::class, function (MockInterface $mock) { - $mock->shouldAllowMockingProtectedMethods() - ->shouldReceive('getLogo') - ->once() - ->andReturn(null); - }); + $logoService = new LogoService(); - $icon = $logoServiceMock->getIcon('no_logo_should_exists_with_this_name'); + $icon = $logoService->getIcon('no_logo_should_exists_with_this_name'); $this->assertEquals(null, $icon); } + + /** + * @test + */ + public function test_logoService_loads_empty_collection_when_tfajson_fetching_fails() + { + $svgLogo = HttpRequestTestData::SVG_LOGO_BODY; + + Http::preventStrayRequests(); + Http::fake([ + 'https://raw.githubusercontent.com/2factorauth/twofactorauth/master/img/*' => Http::response($svgLogo, 200), + ]); + + Storage::fake('icons'); + Storage::fake('logos'); + + $logoService = new LogoService(); + $icon = $logoService->getIcon('twitter'); + + $this->assertNull($icon); + Storage::disk('logos')->assertMissing(LogoService::TFA_JSON); + } } diff --git a/tests/Feature/Services/QrCodeServiceTest.php b/tests/Feature/Services/QrCodeServiceTest.php index c5e5ca59..e0dbea58 100644 --- a/tests/Feature/Services/QrCodeServiceTest.php +++ b/tests/Feature/Services/QrCodeServiceTest.php @@ -8,6 +8,7 @@ /** * @covers \App\Services\QrCodeService + * @covers \App\Facades\QrCode */ class QrCodeServiceTest extends FeatureTestCase { @@ -20,7 +21,7 @@ class QrCodeServiceTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); } diff --git a/tests/Feature/Services/ReleaseRadarServiceTest.php b/tests/Feature/Services/ReleaseRadarServiceTest.php new file mode 100644 index 00000000..ee9a7150 --- /dev/null +++ b/tests/Feature/Services/ReleaseRadarServiceTest.php @@ -0,0 +1,138 @@ + Http::response(HttpRequestTestData::LATEST_RELEASE_BODY_NO_NEW_RELEASE, 200), + ]); + + $releaseRadarService = new ReleaseRadarService(); + $release = $releaseRadarService->manualScan(); + + $this->assertFalse($release); + $this->assertDatabaseHas('options', [ + 'key' => 'lastRadarScan', + ]); + $this->assertDatabaseMissing('options', [ + 'key' => 'latestRelease', + 'value' => HttpRequestTestData::TAG_NAME + ]); + } + + /** + * @test + */ + public function test_manualScan_returns_new_release() + { + $url = config('2fauth.latestReleaseUrl'); + + Http::preventStrayRequests(); + Http::fake([ + $url => Http::response(HttpRequestTestData::LATEST_RELEASE_BODY_NEW_RELEASE, 200), + ]); + + $releaseRadarService = new ReleaseRadarService(); + $release = $releaseRadarService->manualScan(); + + $this->assertEquals(HttpRequestTestData::NEW_TAG_NAME, $release); + $this->assertDatabaseHas('options', [ + 'key' => 'latestRelease', + 'value' => HttpRequestTestData::NEW_TAG_NAME + ]); + $this->assertDatabaseHas('options', [ + 'key' => 'lastRadarScan', + ]); + } + + /** + * @test + */ + public function test_manualScan_succeed_when_something_fails() + { + $url = config('2fauth.latestReleaseUrl'); + + // We do not fake the http request so an exception will be thrown + Http::preventStrayRequests(); + + $releaseRadarService = new ReleaseRadarService(); + $release = $releaseRadarService->manualScan(); + + $this->assertFalse($release); + } + + /** + * @test + */ + public function test_manualScan_succeed_when_github_is_unreachable() + { + $url = config('2fauth.latestReleaseUrl'); + + Http::preventStrayRequests(); + Http::fake([ + $url => Http::response(null, 400), + ]); + + $releaseRadarService = new ReleaseRadarService(); + $release = $releaseRadarService->manualScan(); + + $this->assertFalse($release); + } + + /** + * @test + */ + public function test_scheduleScan_runs_after_one_week() + { + $url = config('2fauth.latestReleaseUrl'); + + Http::preventStrayRequests(); + Http::fake([ + $url => Http::response(HttpRequestTestData::LATEST_RELEASE_BODY_NEW_RELEASE, 200), + ]); + + Settings::set('lastRadarScan', time() - (60 * 60 * 24 * 7) - 1); + + $releaseRadarService = $this->mock(ReleaseRadarService::class)->makePartial(); + $releaseRadarService->shouldAllowMockingProtectedMethods() + ->shouldReceive('newRelease') + ->once(); + + $releaseRadarService->scheduledScan(); + } + + /** + * @test + */ + public function test_scheduleScan_does_not_run_before_one_week() + { + Settings::set('lastRadarScan', time() - (60 * 60 * 24 * 7) + 2); + + $releaseRadarService = $this->mock(ReleaseRadarService::class)->makePartial(); + $releaseRadarService->shouldAllowMockingProtectedMethods() + ->shouldNotReceive('newRelease'); + + $releaseRadarService->scheduledScan(); + } +} diff --git a/tests/Feature/Services/SettingServiceTest.php b/tests/Feature/Services/SettingServiceTest.php index 6b3bab67..8c679fe2 100644 --- a/tests/Feature/Services/SettingServiceTest.php +++ b/tests/Feature/Services/SettingServiceTest.php @@ -10,6 +10,7 @@ /** * @covers \App\Services\SettingService + * @covers \App\Facades\Settings */ class SettingServiceTest extends FeatureTestCase { @@ -57,7 +58,7 @@ class SettingServiceTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); @@ -238,7 +239,7 @@ public function test_set_useEncryption_off_returns_exception_when_data_are_undec /** * Provide invalid data for validation test */ - public function provideUndecipherableData() : array + public function provideUndecipherableData(): array { return [ [[ @@ -316,4 +317,26 @@ public function test_del_remove_setting_from_db() self::VALUE => self::SETTING_VALUE_STRING, ]); } + + /** + * @test + */ + public function test_isUserDefined_returns_true() + { + DB::table('options')->insert( + [self::KEY => 'showTokenAsDot', self::VALUE => strval(self::SETTING_VALUE_TRUE_TRANSFORMED)] + ); + + $this->assertTrue(Settings::isUserDefined('showTokenAsDot')); + } + + /** + * @test + */ + public function test_isUserDefined_returns_false() + { + DB::table('options')->where(self::KEY, 'showTokenAsDot')->delete(); + + $this->assertFalse(Settings::isUserDefined('showTokenAsDot')); + } } diff --git a/tests/Feature/Services/TwoFAccountServiceTest.php b/tests/Feature/Services/TwoFAccountServiceTest.php index 9499e4d7..af7e4a71 100644 --- a/tests/Feature/Services/TwoFAccountServiceTest.php +++ b/tests/Feature/Services/TwoFAccountServiceTest.php @@ -5,11 +5,13 @@ use App\Facades\TwoFAccounts; use App\Models\Group; use App\Models\TwoFAccount; -use Tests\Classes\OtpTestData; +use Tests\Data\OtpTestData; use Tests\FeatureTestCase; +use Tests\Data\MigrationTestData; /** * @covers \App\Services\TwoFAccountService + * @covers \App\Facades\TwoFAccounts */ class TwoFAccountServiceTest extends FeatureTestCase { @@ -31,7 +33,7 @@ class TwoFAccountServiceTest extends FeatureTestCase /** * @test */ - public function setUp() : void + public function setUp(): void { parent::setUp(); @@ -179,7 +181,7 @@ public function test_delete_single_id() */ public function test_convert_migration_from_gauth_returns_correct_accounts() { - $twofaccounts = TwoFAccounts::migrate(OtpTestData::GOOGLE_AUTH_MIGRATION_URI); + $twofaccounts = TwoFAccounts::migrate(MigrationTestData::GOOGLE_AUTH_MIGRATION_URI); $this->assertCount(2, $twofaccounts); @@ -226,7 +228,7 @@ public function test_convert_migration_from_gauth_returns_flagged_duplicates() $twofaccount = new TwoFAccount; $twofaccount->fillWithOtpParameters($parameters)->save(); - $twofaccounts = TwoFAccounts::migrate(OtpTestData::GOOGLE_AUTH_MIGRATION_URI); + $twofaccounts = TwoFAccounts::migrate(MigrationTestData::GOOGLE_AUTH_MIGRATION_URI); $this->assertEquals(-1, $twofaccounts->first()->id); $this->assertEquals(-1, $twofaccounts->last()->id); @@ -238,6 +240,6 @@ public function test_convert_migration_from_gauth_returns_flagged_duplicates() public function test_convert_invalid_migration_from_gauth_returns_InvalidMigrationData_exception() { $this->expectException(\App\Exceptions\InvalidMigrationDataException::class); - $twofaccounts = TwoFAccounts::migrate(OtpTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA); + $twofaccounts = TwoFAccounts::migrate(MigrationTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA); } } diff --git a/tests/Unit/Exceptions/HandlerTest.php b/tests/Unit/Exceptions/HandlerTest.php index 65cbc6e3..d9a8c002 100644 --- a/tests/Unit/Exceptions/HandlerTest.php +++ b/tests/Unit/Exceptions/HandlerTest.php @@ -7,6 +7,15 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Tests\TestCase; +use App\Exceptions\InvalidOtpParameterException; +use \App\Exceptions\InvalidQrCodeException; +use App\Exceptions\InvalidSecretException; +use App\Exceptions\DbEncryptionException; +use App\Exceptions\InvalidMigrationDataException; +use App\Exceptions\UndecipherableException; +use App\Exceptions\UnsupportedMigrationException; +use App\Exceptions\UnsupportedOtpTypeException; +use App\Exceptions\EncryptedMigrationException; /** * @covers \App\Exceptions\Handler @@ -41,32 +50,35 @@ public function test_exceptions_returns_badRequest_json_response($exception) /** * Provide Valid data for validation test */ - public function provideExceptionsforBadRequest() : array + public function provideExceptionsforBadRequest(): array { return [ [ - '\App\Exceptions\InvalidOtpParameterException', + InvalidOtpParameterException::class, ], [ - '\App\Exceptions\InvalidQrCodeException', + InvalidQrCodeException::class, ], [ - '\App\Exceptions\InvalidSecretException', + InvalidSecretException::class, ], [ - '\App\Exceptions\DbEncryptionException', + DbEncryptionException::class, ], [ - '\App\Exceptions\InvalidMigrationDataException', + InvalidMigrationDataException::class, ], [ - '\App\Exceptions\UndecipherableException', + UndecipherableException::class, ], [ - '\App\Exceptions\UnsupportedMigrationException', + UnsupportedMigrationException::class, ], [ - '\App\Exceptions\UnsupportedOtpTypeException', + UnsupportedOtpTypeException::class, + ], + [ + EncryptedMigrationException::class, ], ]; } @@ -99,7 +111,7 @@ public function test_exceptions_returns_notFound_json_response($exception) /** * Provide Valid data for validation test */ - public function provideExceptionsforNotFound() : array + public function provideExceptionsforNotFound(): array { return [ [ @@ -111,6 +123,32 @@ public function provideExceptionsforNotFound() : array ]; } + /** + * @test + */ + public function test_authenticationException_returns_unauthorized_json_response() + { + $request = $this->createMock(Request::class); + $instance = new Handler($this->createMock(Container::class)); + $class = new \ReflectionClass(Handler::class); + + $method = $class->getMethod('render'); + $method->setAccessible(true); + + $mockException = $this->createMock(\Illuminate\Auth\AuthenticationException::class); + $mockException->method('guards')->willReturn(['web-guard']); + + $response = $method->invokeArgs($instance, [$request, $mockException]); + + $this->assertInstanceOf(JsonResponse::class, $response); + + $response = \Illuminate\Testing\TestResponse::fromBaseResponse($response); + $response->assertStatus(401) + ->assertJsonStructure([ + 'message', + ]); + } + /** * @test */ diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php new file mode 100644 index 00000000..96db7256 --- /dev/null +++ b/tests/Unit/HelpersTest.php @@ -0,0 +1,98 @@ +assertIsString($filename); + $this->assertStringEndsWith('.' . $ext, $filename); + $this->assertEquals(41 + strlen($ext), strlen($filename)); + } + + /** + * @test + * + * @dataProvider versionNumberProvider + */ + public function test_cleanVersionNumber_returns_cleaned_version($dirtyVersion, $expected) + { + $cleanedVersion = Helpers::cleanVersionNumber($dirtyVersion); + + $this->assertEquals($expected, $cleanedVersion); + } + + /** + * Provide data for cleanVersionNumber() tests + */ + public function versionNumberProvider() + { + return [ + [ + 'v3.2.1', + '3.2.1', + ], + [ + 'v3.2.1-beta', + '3.2.1-beta', + ], + [ + 'v3.0.1-alpha+001', + '3.0.1-alpha+001', + ], + [ + 'version03.0.1 alpha+001', + '3.0.1', + ], + ]; + } + + /** + * @test + * + * @dataProvider invalidVersionNumberProvider + */ + public function test_cleanVersionNumber_returns_false_with_invalid_semver($dirtyVersion) + { + $cleanedVersion = Helpers::cleanVersionNumber($dirtyVersion); + + $this->assertEquals(false, $cleanedVersion); + } + + /** + * Provide data for cleanVersionNumber() tests + */ + public function invalidVersionNumberProvider() + { + return [ + [ + 'v3.2.', + ], + [ + 'v3..1-beta', + ], + [ + 'v.0.1-alpha+001', + ], + [ + '3.00.1 alpha+001', + ], + [ + '3.00.1 alpha+001', + ], + ]; + } +} diff --git a/tests/Unit/Listeners/CleanIconStorageTest.php b/tests/Unit/Listeners/CleanIconStorageTest.php index 9f9fc8d0..690d26ef 100644 --- a/tests/Unit/Listeners/CleanIconStorageTest.php +++ b/tests/Unit/Listeners/CleanIconStorageTest.php @@ -16,6 +16,9 @@ */ class CleanIconStorageTest extends TestCase { + /** + * @test + */ public function test_it_deletes_icon_file_on_twofaccount_deletion() { $settingService = $this->mock(SettingService::class, function (MockInterface $settingService) { @@ -34,6 +37,9 @@ public function test_it_deletes_icon_file_on_twofaccount_deletion() $this->assertNull($listener->handle($event)); } + /** + * @test + */ public function test_CleanIconStorage_listen_to_TwoFAccountDeleted_event() { Event::fake(); diff --git a/tests/Unit/Listeners/DissociateTwofaccountFromGroupTest.php b/tests/Unit/Listeners/DissociateTwofaccountFromGroupTest.php index 84c08dbb..bf807844 100644 --- a/tests/Unit/Listeners/DissociateTwofaccountFromGroupTest.php +++ b/tests/Unit/Listeners/DissociateTwofaccountFromGroupTest.php @@ -5,23 +5,41 @@ use App\Events\GroupDeleting; use App\Listeners\DissociateTwofaccountFromGroup; use App\Models\Group; +use App\Models\TwoFAccount; use Illuminate\Support\Facades\Event; use Tests\TestCase; +use Mockery\MockInterface; /** * @covers \App\Listeners\DissociateTwofaccountFromGroup */ class DissociateTwofaccountFromGroupTest extends TestCase { - // public function test_twofaccount_is_released_on_group_deletion() - // { - // $group = Group::factory()->make(); - // $event = new GroupDeleting($group); - // $listener = new DissociateTwofaccountFromGroup(); + /** + * @test + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_twofaccount_is_released_on_group_deletion() + { - // $this->assertNull($listener->handle($event)); - // } + $this->mock('alias:' . TwoFAccount::class, function (MockInterface $twoFAccount) { + $twoFAccount->shouldReceive('where->update') + ->once() + ->andReturn(1); + }); + $group = Group::factory()->make(); + $event = new GroupDeleting($group); + $listener = new DissociateTwofaccountFromGroup(); + + $this->assertNull($listener->handle($event)); + } + + /** + * @test + */ public function test_DissociateTwofaccountFromGroup_listen_to_groupDeleting_event() { Event::fake(); diff --git a/tests/Unit/MigratorTest.php b/tests/Unit/MigratorTest.php new file mode 100644 index 00000000..6f8773b2 --- /dev/null +++ b/tests/Unit/MigratorTest.php @@ -0,0 +1,484 @@ +mock(SettingService::class, function (MockInterface $settingService) { + $settingService->allows() + ->get('useEncryption') + ->andReturn(false); + + $settingService->allows() + ->get('getOfficialIcons') + ->andReturn(false); + }); + + $this->totpTwofaccount = new TwoFAccount; + $this->totpTwofaccount->legacy_uri = OtpTestData::TOTP_FULL_CUSTOM_URI_NO_IMG; + $this->totpTwofaccount->service = OtpTestData::SERVICE; + $this->totpTwofaccount->account = OtpTestData::ACCOUNT; + $this->totpTwofaccount->icon = null; + $this->totpTwofaccount->otp_type = 'totp'; + $this->totpTwofaccount->secret = OtpTestData::SECRET; + $this->totpTwofaccount->digits = OtpTestData::DIGITS_CUSTOM; + $this->totpTwofaccount->algorithm = OtpTestData::ALGORITHM_CUSTOM; + $this->totpTwofaccount->period = OtpTestData::PERIOD_CUSTOM; + $this->totpTwofaccount->counter = null; + + $this->hotpTwofaccount = new TwoFAccount; + $this->hotpTwofaccount->legacy_uri = OtpTestData::HOTP_FULL_CUSTOM_URI_NO_IMG; + $this->hotpTwofaccount->service = OtpTestData::SERVICE; + $this->hotpTwofaccount->account = OtpTestData::ACCOUNT; + $this->hotpTwofaccount->icon = null; + $this->hotpTwofaccount->otp_type = 'hotp'; + $this->hotpTwofaccount->secret = OtpTestData::SECRET; + $this->hotpTwofaccount->digits = OtpTestData::DIGITS_CUSTOM; + $this->hotpTwofaccount->algorithm = OtpTestData::ALGORITHM_CUSTOM; + $this->hotpTwofaccount->period = null; + $this->hotpTwofaccount->counter = OtpTestData::COUNTER_CUSTOM; + + $this->steamTwofaccount = new TwoFAccount; + $this->steamTwofaccount->legacy_uri = OtpTestData::STEAM_TOTP_URI; + $this->steamTwofaccount->service = OtpTestData::STEAM; + $this->steamTwofaccount->account = OtpTestData::ACCOUNT; + $this->steamTwofaccount->icon = null; + $this->steamTwofaccount->otp_type = 'steamtotp'; + $this->steamTwofaccount->secret = OtpTestData::STEAM_SECRET; + $this->steamTwofaccount->digits = OtpTestData::DIGITS_STEAM; + $this->steamTwofaccount->algorithm = OtpTestData::ALGORITHM_DEFAULT; + $this->steamTwofaccount->period = OtpTestData::PERIOD_DEFAULT; + $this->steamTwofaccount->counter = null; + + $this->GAuthTotpTwofaccount = new TwoFAccount; + $this->GAuthTotpTwofaccount->service = OtpTestData::SERVICE; + $this->GAuthTotpTwofaccount->account = OtpTestData::ACCOUNT; + $this->GAuthTotpTwofaccount->icon = null; + $this->GAuthTotpTwofaccount->otp_type = 'totp'; + $this->GAuthTotpTwofaccount->secret = OtpTestData::SECRET; + $this->GAuthTotpTwofaccount->digits = OtpTestData::DIGITS_DEFAULT; + $this->GAuthTotpTwofaccount->algorithm = OtpTestData::ALGORITHM_DEFAULT; + $this->GAuthTotpTwofaccount->period = OtpTestData::PERIOD_DEFAULT; + $this->GAuthTotpTwofaccount->counter = null; + + $this->GAuthTotpBisTwofaccount = new TwoFAccount; + $this->GAuthTotpBisTwofaccount->service = OtpTestData::SERVICE . '_bis'; + $this->GAuthTotpBisTwofaccount->account = OtpTestData::ACCOUNT . '_bis'; + $this->GAuthTotpBisTwofaccount->icon = null; + $this->GAuthTotpBisTwofaccount->otp_type = 'totp'; + $this->GAuthTotpBisTwofaccount->secret = OtpTestData::SECRET; + $this->GAuthTotpBisTwofaccount->digits = OtpTestData::DIGITS_DEFAULT; + $this->GAuthTotpBisTwofaccount->algorithm = OtpTestData::ALGORITHM_DEFAULT; + $this->GAuthTotpBisTwofaccount->period = OtpTestData::PERIOD_DEFAULT; + $this->GAuthTotpBisTwofaccount->counter = null; + + $this->fakeTwofaccount = new TwoFAccount; + $this->fakeTwofaccount->id = TwoFAccount::FAKE_ID; + } + + /** + * @test + * + * @dataProvider validMigrationsProvider + */ + public function test_migrate_returns_consistent_accounts(Migrator $migrator, mixed $payload, string $expected, bool $hasSteam) + { + $accounts = $migrator->migrate($payload); + + if ($expected === 'gauth') { + $totp = $this->GAuthTotpTwofaccount; + $hotp = $this->GAuthTotpBisTwofaccount; + } else { + $totp = $this->totpTwofaccount; + $hotp = $this->hotpTwofaccount; + if ($hasSteam) { + $steam = $this->steamTwofaccount; + } + } + + $this->assertContainsOnlyInstancesOf(TwoFAccount::class, $accounts); + $this->assertCount($hasSteam ? 3 : 2, $accounts); + + // The returned collection could have non-linear index (because of possible blank lines + // in the migration payload) so we do not use get() to retrieve items + $this->assertObjectEquals($totp, $accounts->first()); + $this->assertObjectEquals($hotp, $accounts->slice(1, 1)->first()); + if ($hasSteam) { + $this->assertObjectEquals($steam, $accounts->last()); + } + } + + /** + * Provide data for TwoFAccount store tests + */ + public function validMigrationsProvider() + { + return [ + 'PLAIN_TEXT_PAYLOAD' => [ + new PlainTextMigrator(), + MigrationTestData::VALID_PLAIN_TEXT_PAYLOAD, + 'custom', + $hasSteam = true + ], + 'PLAIN_TEXT_PAYLOAD_WITH_INTRUDER' => [ + new PlainTextMigrator(), + MigrationTestData::VALID_PLAIN_TEXT_PAYLOAD_WITH_INTRUDER, + 'custom', + $hasSteam = true + ], + 'AEGIS_JSON_MIGRATION_PAYLOAD' => [ + new AegisMigrator(), + MigrationTestData::VALID_AEGIS_JSON_MIGRATION_PAYLOAD, + 'custom', + $hasSteam = true + ], + '2FAS_MIGRATION_PAYLOAD' => [ + new TwoFASMigrator(), + MigrationTestData::VALID_2FAS_MIGRATION_PAYLOAD, + 'custom', + $hasSteam = false + ], + 'GOOGLE_AUTH_MIGRATION_PAYLOAD' => [ + new GoogleAuthMigrator(), + MigrationTestData::GOOGLE_AUTH_MIGRATION_URI, + 'gauth', + $hasSteam = false, + ], + ]; + } + + /** + * @test + * + * @dataProvider invalidMigrationsProvider + */ + public function test_migrate_with_invalid_payload_returns_InvalidMigrationDataException(Migrator $migrator, mixed $payload) + { + $this->expectException(InvalidMigrationDataException::class); + + $accounts = $migrator->migrate($payload); + } + + /** + * Provide data for TwoFAccount store tests + */ + public function invalidMigrationsProvider() + { + return [ + 'INVALID_PLAIN_TEXT_NO_URI' => [ + new PlainTextMigrator(), + MigrationTestData::INVALID_PLAIN_TEXT_NO_URI, + ], + 'INVALID_PLAIN_TEXT_ONLY_EMPTY_LINES' => [ + new PlainTextMigrator(), + MigrationTestData::INVALID_PLAIN_TEXT_ONLY_EMPTY_LINES, + ], + 'INVALID_PLAIN_TEXT_NULL' => [ + new PlainTextMigrator(), + null, + ], + 'INVALID_PLAIN_TEXT_EMPTY_STRING' => [ + new PlainTextMigrator(), + '', + ], + 'INVALID_PLAIN_TEXT_INT' => [ + new PlainTextMigrator(), + 10, + ], + 'INVALID_PLAIN_TEXT_BOOL' => [ + new PlainTextMigrator(), + true, + ], + 'INVALID_AEGIS_JSON_MIGRATION_PAYLOAD' => [ + new AegisMigrator(), + MigrationTestData::INVALID_AEGIS_JSON_MIGRATION_PAYLOAD, + ], + 'ENCRYPTED_AEGIS_JSON_MIGRATION_PAYLOAD' => [ + new AegisMigrator(), + MigrationTestData::ENCRYPTED_AEGIS_JSON_MIGRATION_PAYLOAD, + ], + 'INVALID_2FAS_MIGRATION_PAYLOAD' => [ + new TwoFASMigrator(), + MigrationTestData::INVALID_2FAS_MIGRATION_PAYLOAD, + ], + 'INVALID_GOOGLE_AUTH_MIGRATION_URI' => [ + new GoogleAuthMigrator(), + MigrationTestData::INVALID_GOOGLE_AUTH_MIGRATION_URI, + ], + 'GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA' => [ + new GoogleAuthMigrator(), + MigrationTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA, + ], + + ]; + } + + /** + * @test + * + * @dataProvider migrationWithInvalidAccountsProvider + */ + public function test_migrate_returns_fake_accounts(Migrator $migrator, mixed $payload) + { + $accounts = $migrator->migrate($payload); + + $this->assertContainsOnlyInstancesOf(TwoFAccount::class, $accounts); + $this->assertCount(2, $accounts); + + // The returned collection could have non-linear index (because of possible blank lines + // in the migration payload) so we do not use get() to retrieve items + $this->assertObjectEquals($this->totpTwofaccount, $accounts->first()); + $this->assertEquals($this->fakeTwofaccount->id, $accounts->last()->id); + } + + /** + * Provide data for TwoFAccount store tests + */ + public function migrationWithInvalidAccountsProvider() + { + return [ + 'PLAIN_TEXT_PAYLOAD_WITH_INVALID_URI' => [ + new PlainTextMigrator(), + MigrationTestData::PLAIN_TEXT_PAYLOAD_WITH_INVALID_URI, + ], + 'VALID_AEGIS_JSON_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_OTP_TYPE' => [ + new AegisMigrator(), + MigrationTestData::VALID_AEGIS_JSON_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_OTP_TYPE, + ], + 'VALID_2FAS_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_OTP_TYPE' => [ + new TwoFASMigrator(), + MigrationTestData::VALID_2FAS_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_OTP_TYPE, + ], + ]; + } + + /** + * @test + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_migrate_gauth_returns_fake_accounts() + { + $this->mock('alias:' . Base32::class, function (MockInterface $baseEncoder) { + $baseEncoder->shouldReceive('encodeUpper') + ->andThrow(new \Exception()); + }); + + $migrator = new GoogleAuthMigrator(); + $accounts = $migrator->migrate(MigrationTestData::GOOGLE_AUTH_MIGRATION_URI); + + $this->assertContainsOnlyInstancesOf(TwoFAccount::class, $accounts); + $this->assertCount(2, $accounts); + + // The returned collection could have non-linear index (because of possible blank lines + // in the migration payload) so we do not use get() to retrieve items + $this->assertEquals($this->fakeTwofaccount->id, $accounts->first()->id); + $this->assertEquals($this->fakeTwofaccount->id, $accounts->last()->id); + } + + /** + * @test + * + * @dataProvider AegisWithIconMigrationProvider + */ + public function test_migrate_aegis_payload_with_icon_sets_and_stores_the_icon($migration) + { + Storage::fake('icons'); + + $migrator = new AegisMigrator(); + $accounts = $migrator->migrate($migration); + + $this->assertContainsOnlyInstancesOf(TwoFAccount::class, $accounts); + $this->assertCount(1, $accounts); + + Storage::disk('icons')->assertExists($accounts->first()->icon); + } + + /** + * Provide data for TwoFAccount store tests + */ + public function AegisWithIconMigrationProvider() + { + return [ + 'SVG' => [ + MigrationTestData::VALID_AEGIS_JSON_MIGRATION_PAYLOAD_WITH_SVG_ICON, + ], + 'PNG' => [ + MigrationTestData::VALID_AEGIS_JSON_MIGRATION_PAYLOAD_WITH_PNG_ICON, + ], + 'JPG' => [ + MigrationTestData::VALID_AEGIS_JSON_MIGRATION_PAYLOAD_WITH_JPG_ICON, + ], + ]; + } + + /** + * @test + */ + public function test_migrate_aegis_payload_with_unsupported_icon_does_not_fail() + { + Storage::fake('icons'); + + $migrator = new AegisMigrator(); + $accounts = $migrator->migrate(MigrationTestData::VALID_AEGIS_JSON_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_ICON); + + $this->assertContainsOnlyInstancesOf(TwoFAccount::class, $accounts); + $this->assertCount(1, $accounts); + + $this->assertNull($this->fakeTwofaccount->icon); + Storage::disk('icons')->assertDirectoryEmpty('/'); + } + + /** + * @test + * + * @dataProvider factoryProvider + */ + public function test_factory_returns_plain_text_migrator($payload, $migratorClass) + { + $factory = new MigratorFactory(); + + $migrator = $factory->create($payload); + + $this->assertInstanceOf($migratorClass, $migrator); + } + + /** + * Provide data for TwoFAccount store tests + */ + public function factoryProvider() + { + return [ + 'VALID_PLAIN_TEXT_PAYLOAD' => [ + MigrationTestData::VALID_PLAIN_TEXT_PAYLOAD, + PlainTextMigrator::class, + ], + 'VALID_AEGIS_JSON_MIGRATION_PAYLOAD' => [ + MigrationTestData::VALID_AEGIS_JSON_MIGRATION_PAYLOAD, + AegisMigrator::class, + ], + 'VALID_AEGIS_JSON_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_ICON' => [ + MigrationTestData::VALID_AEGIS_JSON_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_ICON, + AegisMigrator::class, + ], + 'VALID_2FAS_MIGRATION_PAYLOAD' => [ + MigrationTestData::VALID_2FAS_MIGRATION_PAYLOAD, + TwoFASMigrator::class, + ], + 'GOOGLE_AUTH_MIGRATION_URI' => [ + MigrationTestData::GOOGLE_AUTH_MIGRATION_URI, + GoogleAuthMigrator::class, + ], + ]; + } + + /** + * @test + */ + public function test_factory_throw_UnsupportedMigrationException() + { + $this->expectException(UnsupportedMigrationException::class); + $factory = new MigratorFactory(); + + $migrator = $factory->create('not_a_valid_payload'); + } + + /** + * @test + * + * @dataProvider encryptedMigrationDataProvider + */ + public function test_factory_throw_EncryptedMigrationException($payload) + { + $this->expectException(EncryptedMigrationException::class); + + $factory = new MigratorFactory(); + + $migrator = $factory->create($payload); + } + + /** + * Provide data for TwoFAccount store tests + */ + public function encryptedMigrationDataProvider() + { + return [ + 'ENCRYPTED_AEGIS_JSON_MIGRATION_PAYLOAD' => [ + MigrationTestData::ENCRYPTED_AEGIS_JSON_MIGRATION_PAYLOAD + ], + 'ENCRYPTED_2FAS_MIGRATION_PAYLOAD' => [ + MigrationTestData::ENCRYPTED_2FAS_MIGRATION_PAYLOAD + ], + ]; + } + + /** + * + */ + protected function tearDown(): void + { + Mockery::close(); + } +}