123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- <?php
- namespace Tests\Unit;
- use App\Exceptions\EncryptedMigrationException;
- use App\Exceptions\InvalidMigrationDataException;
- use App\Exceptions\UnsupportedMigrationException;
- use App\Factories\MigratorFactory;
- use App\Models\TwoFAccount;
- use App\Services\LogoService;
- use App\Services\Migrators\AegisMigrator;
- use App\Services\Migrators\GoogleAuthMigrator;
- use App\Services\Migrators\Migrator;
- use App\Services\Migrators\PlainTextMigrator;
- use App\Services\Migrators\TwoFASMigrator;
- use App\Services\Migrators\TwoFAuthMigrator;
- use App\Services\SettingService;
- use Illuminate\Support\Facades\Storage;
- use Mockery;
- use Mockery\MockInterface;
- use ParagonIE\ConstantTime\Base32;
- use Tests\Data\MigrationTestData;
- use Tests\Data\OtpTestData;
- use Tests\TestCase;
- /**
- * @covers \App\Providers\MigrationServiceProvider
- * @covers \App\Factories\MigratorFactory
- * @covers \App\Services\Migrators\Migrator
- * @covers \App\Services\Migrators\AegisMigrator
- * @covers \App\Services\Migrators\TwoFASMigrator
- * @covers \App\Services\Migrators\PlainTextMigrator
- * @covers \App\Services\Migrators\GoogleAuthMigrator
- * @covers \App\Services\Migrators\TwoFAuthMigrator
- *
- * @uses \App\Models\TwoFAccount
- */
- class MigratorTest extends TestCase
- {
- /**
- * App\Models\TwoFAccount $totpTwofaccount
- */
- protected $totpTwofaccount;
- /**
- * App\Models\TwoFAccount $totpTwofaccount
- */
- protected $hotpTwofaccount;
- /**
- * App\Models\TwoFAccount $steamTwofaccount
- */
- protected $steamTwofaccount;
- /**
- * App\Models\TwoFAccount $GAuthTotpTwofaccount
- */
- protected $GAuthTotpTwofaccount;
- /**
- * App\Models\TwoFAccount $GAuthTotpBisTwofaccount
- */
- protected $GAuthTotpBisTwofaccount;
- protected $fakeTwofaccount;
- public function setUp() : void
- {
- parent::setUp();
- $this->mock(SettingService::class, function (MockInterface $settingService) {
- $settingService->allows()
- ->get('useEncryption')
- ->andReturn(false);
- });
- $this->mock(LogoService::class, function (MockInterface $logoService) {
- $logoService->allows([
- 'getIcon' => null,
- ]);
- });
- $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,
- ],
- '2FAUTH_MIGRATION_PAYLOAD' => [
- new TwoFAuthMigrator(),
- MigrationTestData::VALID_2FAUTH_JSON_MIGRATION_PAYLOAD,
- 'custom',
- $hasSteam = true,
- ],
- ];
- }
- /**
- * @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,
- ],
- 'INVALID_2FAUTH_JSON_MIGRATION_PAYLOAD' => [
- new TwoFAuthMigrator(),
- MigrationTestData::INVALID_2FAUTH_JSON_MIGRATION_PAYLOAD,
- ],
- ];
- }
- /**
- * @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,
- ],
- 'VALID_2FAUTH_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_OTP_TYPE' => [
- new TwoFAuthMigrator(),
- MigrationTestData::VALID_2FAUTH_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 TwoFAuthWithIconMigrationProvider
- */
- public function test_migrate_2fauth_payload_with_icon_sets_and_stores_the_icon($migration)
- {
- Storage::fake('icons');
- $migrator = new TwoFAuthMigrator();
- $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 TwoFAuthWithIconMigrationProvider()
- {
- return [
- 'SVG' => [
- MigrationTestData::VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_SVG_ICON,
- ],
- 'PNG' => [
- MigrationTestData::VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_PNG_ICON,
- ],
- 'JPG' => [
- MigrationTestData::VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_JPG_ICON,
- ],
- 'BMP' => [
- MigrationTestData::VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_BMP_ICON,
- ],
- 'WEBP' => [
- MigrationTestData::VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_WEBP_ICON,
- ],
- ];
- }
- /**
- * @test
- */
- public function test_migrate_2fauth_payload_with_unsupported_icon_does_not_fail()
- {
- Storage::fake('icons');
- $migrator = new TwoFAuthMigrator();
- $accounts = $migrator->migrate(MigrationTestData::VALID_2FAUTH_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_relevant_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,
- ],
- '2FAUTH_MIGRATION_URI' => [
- MigrationTestData::VALID_2FAUTH_JSON_MIGRATION_PAYLOAD,
- TwoFAuthMigrator::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();
- }
- }
|