Enhance test coverage

This commit is contained in:
Bubka 2023-03-18 17:33:43 +01:00
parent c717e6b279
commit 9c5f18bb46
17 changed files with 844 additions and 73 deletions

View File

@ -21,7 +21,7 @@ class WebauthnTwoFAuthUserProvider extends WebAuthnUserProvider
return $this->validateWebAuthn(); return $this->validateWebAuthn();
} }
// If the user disabled the fallback is enabled, we will validate the credential password. // If the user disabled the fallback, we will validate the credential password.
return $user->preferences['useWebauthnOnly'] == false && EloquentUserProvider::validateCredentials($user, $credentials); return $user->preferences['useWebauthnOnly'] == false && EloquentUserProvider::validateCredentials($user, $credentials);
} }
} }

View File

@ -17,10 +17,10 @@ class GroupPolicy
* @param \App\Models\User $user * @param \App\Models\User $user
* @return \Illuminate\Auth\Access\Response|bool * @return \Illuminate\Auth\Access\Response|bool
*/ */
public function viewAny(User $user) // public function viewAny(User $user)
{ // {
return false; // return false;
} // }
/** /**
* Determine whether the user can view the model. * Determine whether the user can view the model.
@ -48,19 +48,19 @@ class GroupPolicy
* @param \Illuminate\Support\Collection<int, \App\Models\Group> $groups * @param \Illuminate\Support\Collection<int, \App\Models\Group> $groups
* @return \Illuminate\Auth\Access\Response|bool * @return \Illuminate\Auth\Access\Response|bool
*/ */
public function viewEach(User $user, Group $group, $groups) // public function viewEach(User $user, Group $group, $groups)
{ // {
$can = $this->isOwnerOfEach($user, $groups); // $can = $this->isOwnerOfEach($user, $groups);
if (! $can) { // if (! $can) {
$ids = $groups->map(function ($group, $key) { // $ids = $groups->map(function ($group, $key) {
return $group->id; // return $group->id;
}); // });
Log::notice(sprintf('User ID #%s cannot view all groups in IDs #%s', $user->id, implode(',', $ids->toArray()))); // Log::notice(sprintf('User ID #%s cannot view all groups in IDs #%s', $user->id, implode(',', $ids->toArray())));
} // }
return $can; // return $can;
} // }
/** /**
* Determine whether the user can create models. * Determine whether the user can create models.
@ -101,19 +101,19 @@ class GroupPolicy
* @param \Illuminate\Support\Collection<int, \App\Models\Group> $groups * @param \Illuminate\Support\Collection<int, \App\Models\Group> $groups
* @return \Illuminate\Auth\Access\Response|bool * @return \Illuminate\Auth\Access\Response|bool
*/ */
public function updateEach(User $user, Group $group, $groups) // public function updateEach(User $user, Group $group, $groups)
{ // {
$can = $this->isOwnerOfEach($user, $groups); // $can = $this->isOwnerOfEach($user, $groups);
if (! $can) { // if (! $can) {
$ids = $groups->map(function ($group, $key) { // $ids = $groups->map(function ($group, $key) {
return $group->id; // return $group->id;
}); // });
Log::notice(sprintf('User ID #%s cannot update all groups in IDs #%s', $user->id, implode(',', $ids->toArray()))); // Log::notice(sprintf('User ID #%s cannot update all groups in IDs #%s', $user->id, implode(',', $ids->toArray())));
} // }
return $can; // return $can;
} // }
/** /**
* Determine whether the user can delete the model. * Determine whether the user can delete the model.
@ -141,19 +141,19 @@ class GroupPolicy
* @param \Illuminate\Support\Collection<int, \App\Models\Group> $groups * @param \Illuminate\Support\Collection<int, \App\Models\Group> $groups
* @return \Illuminate\Auth\Access\Response|bool * @return \Illuminate\Auth\Access\Response|bool
*/ */
public function deleteEach(User $user, Group $group, $groups) // public function deleteEach(User $user, Group $group, $groups)
{ // {
$can = $this->isOwnerOfEach($user, $groups); // $can = $this->isOwnerOfEach($user, $groups);
if (! $can) { // if (! $can) {
$ids = $groups->map(function ($group, $key) { // $ids = $groups->map(function ($group, $key) {
return $group->id; // return $group->id;
}); // });
Log::notice(sprintf('User ID #%s cannot delete all groups in IDs #%s', $user->id, implode(',', $ids->toArray()))); // Log::notice(sprintf('User ID #%s cannot delete all groups in IDs #%s', $user->id, implode(',', $ids->toArray())));
} // }
return $can; // return $can;
} // }
/** /**
* Determine whether the user can restore the model. * Determine whether the user can restore the model.
@ -162,10 +162,10 @@ class GroupPolicy
* @param \App\Models\Group $group * @param \App\Models\Group $group
* @return \Illuminate\Auth\Access\Response|bool * @return \Illuminate\Auth\Access\Response|bool
*/ */
public function restore(User $user, Group $group) // public function restore(User $user, Group $group)
{ // {
return $this->isOwnerOf($user, $group);
} // }
/** /**
* Determine whether the user can permanently delete the model. * Determine whether the user can permanently delete the model.
@ -174,8 +174,8 @@ class GroupPolicy
* @param \App\Models\Group $group * @param \App\Models\Group $group
* @return \Illuminate\Auth\Access\Response|bool * @return \Illuminate\Auth\Access\Response|bool
*/ */
public function forceDelete(User $user, Group $group) // public function forceDelete(User $user, Group $group)
{ // {
return $this->isOwnerOf($user, $group);
} // }
} }

View File

@ -17,10 +17,10 @@ class TwoFAccountPolicy
* @param \App\Models\User $user * @param \App\Models\User $user
* @return \Illuminate\Auth\Access\Response|bool * @return \Illuminate\Auth\Access\Response|bool
*/ */
public function viewAny(User $user) // public function viewAny(User $user)
{ // {
return false; // return false;
} // }
/** /**
* Determine whether the user can view the model. * Determine whether the user can view the model.
@ -162,10 +162,10 @@ class TwoFAccountPolicy
* @param \App\Models\TwoFAccount $twofaccount * @param \App\Models\TwoFAccount $twofaccount
* @return \Illuminate\Auth\Access\Response|bool * @return \Illuminate\Auth\Access\Response|bool
*/ */
public function restore(User $user, TwoFAccount $twofaccount) // public function restore(User $user, TwoFAccount $twofaccount)
{ // {
return $this->isOwnerOf($user, $twofaccount);
} // }
/** /**
* Determine whether the user can permanently delete the model. * Determine whether the user can permanently delete the model.
@ -174,8 +174,8 @@ class TwoFAccountPolicy
* @param \App\Models\TwoFAccount $twofaccount * @param \App\Models\TwoFAccount $twofaccount
* @return \Illuminate\Auth\Access\Response|bool * @return \Illuminate\Auth\Access\Response|bool
*/ */
public function forceDelete(User $user, TwoFAccount $twofaccount) // public function forceDelete(User $user, TwoFAccount $twofaccount)
{ // {
return $this->isOwnerOf($user, $twofaccount);
} // }
} }

View File

@ -45,7 +45,7 @@ class TwoFAuthMigrator extends Migrator
if (is_null($json)) { if (is_null($json)) {
Log::error('2FAuth JSON migration data cannot be read'); Log::error('2FAuth JSON migration data cannot be read');
throw new InvalidMigrationDataException('2FAS Auth'); throw new InvalidMigrationDataException('2FAuth');
} }
$twofaccounts = []; $twofaccounts = [];

View File

@ -10,6 +10,9 @@ use Tests\FeatureTestCase;
/** /**
* @covers \App\Api\v1\Controllers\GroupController * @covers \App\Api\v1\Controllers\GroupController
* @covers \App\Api\v1\Resources\GroupResource * @covers \App\Api\v1\Resources\GroupResource
* @covers \App\Listeners\ResetUsersPreference
* @covers \App\Policies\GroupPolicy
* @covers \App\Models\Group
*/ */
class GroupControllerTest extends FeatureTestCase class GroupControllerTest extends FeatureTestCase
{ {
@ -444,4 +447,27 @@ class GroupControllerTest extends FeatureTestCase
'message', 'message',
]); ]);
} }
/**
* @test
*/
public function test_destroy_group_resets_user_preferences()
{
// Set the default group to a specific one
$this->user['preferences->defaultGroup'] = $this->userGroupA->id;
// Set the active group
$this->user['preferences->activeGroup'] = $this->userGroupA->id;
$this->user->save();
$this->assertEquals($this->userGroupA->id, $this->user->preferences['defaultGroup']);
$this->assertEquals($this->userGroupA->id, $this->user->preferences['activeGroup']);
$this->actingAs($this->user, 'api-guard')
->json('DELETE', '/api/v1/groups/' . $this->userGroupA->id);
$this->user->refresh();
$this->assertEquals(0, $this->user->preferences['defaultGroup']);
$this->assertEquals(0, $this->user->preferences['activeGroup']);
}
} }

View File

@ -15,10 +15,14 @@ use Tests\FeatureTestCase;
/** /**
* @covers \App\Api\v1\Controllers\TwoFAccountController * @covers \App\Api\v1\Controllers\TwoFAccountController
* @covers \App\Api\v1\Resources\TwoFAccountCollection
* @covers \App\Api\v1\Resources\TwoFAccountReadResource * @covers \App\Api\v1\Resources\TwoFAccountReadResource
* @covers \App\Api\v1\Resources\TwoFAccountStoreResource * @covers \App\Api\v1\Resources\TwoFAccountStoreResource
* @covers \App\Api\v1\Resources\TwoFAccountExportResource
* @covers \App\Api\v1\Resources\TwoFAccountExportCollection
* @covers \App\Providers\MigrationServiceProvider * @covers \App\Providers\MigrationServiceProvider
* @covers \App\Providers\TwoFAuthServiceProvider * @covers \App\Providers\TwoFAuthServiceProvider
* @covers \App\Policies\TwoFAccountPolicy
*/ */
class TwoFAccountControllerTest extends FeatureTestCase class TwoFAccountControllerTest extends FeatureTestCase
{ {
@ -91,6 +95,27 @@ class TwoFAccountControllerTest extends FeatureTestCase
'counter', 'counter',
]; ];
private const VALID_EXPORT_STRUTURE = [
'app',
'schema',
'datetime',
'data' => [
'*' => [
'otp_type',
'account',
'service',
'icon',
'icon_mime',
'icon_file',
'secret',
'digits',
'algorithm',
'period',
'counter',
'legacy_uri',
], ],
];
private const JSON_FRAGMENTS_FOR_CUSTOM_TOTP = [ private const JSON_FRAGMENTS_FOR_CUSTOM_TOTP = [
'service' => OtpTestData::SERVICE, 'service' => OtpTestData::SERVICE,
'account' => OtpTestData::ACCOUNT, 'account' => OtpTestData::ACCOUNT,
@ -868,6 +893,65 @@ class TwoFAccountControllerTest extends FeatureTestCase
]); ]);
} }
/**
* @test
*/
public function test_export_returns_json_migration_resource()
{
$this->twofaccountA = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
$this->twofaccountB = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
$this->actingAs($this->user, 'api-guard')
->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountB->id)
->assertOk()
->assertJsonStructure(self::VALID_EXPORT_STRUTURE)
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP)
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
}
/**
* @test
*/
public function test_export_too_many_ids_returns_bad_request()
{
TwoFAccount::factory()->count(102)->for($this->user)->create();
$ids = DB::table('twofaccounts')->where('user_id', $this->user->id)->pluck('id')->implode(',');
$response = $this->actingAs($this->user, 'api-guard')
->json('GET', '/api/v1/twofaccounts/export?ids=' . $ids)
->assertStatus(400)
->assertJsonStructure([
'message',
'reason',
]);
}
/**
* @test
*/
public function test_export_missing_twofaccount_returns_existing_ones_only()
{
$this->twofaccountA = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
$response = $this->actingAs($this->user, 'api-guard')
->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountA->id . ',1000')
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
}
/**
* @test
*/
public function test_export_twofaccount_of_another_user_is_forbidden()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountC->id)
->assertForbidden()
->assertJsonStructure([
'message',
]);
}
/** /**
* @test * @test
*/ */
@ -1155,8 +1239,6 @@ class TwoFAccountControllerTest extends FeatureTestCase
{ {
TwoFAccount::factory()->count(3)->for($this->user)->create(); TwoFAccount::factory()->count(3)->for($this->user)->create();
$ids = DB::table('twofaccounts')->where('user_id', $this->user->id)->pluck('id')->implode(',');
$response = $this->actingAs($this->user, 'api-guard') $response = $this->actingAs($this->user, 'api-guard')
->json('DELETE', '/api/v1/twofaccounts?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountB->id) ->json('DELETE', '/api/v1/twofaccounts?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountB->id)
->assertNoContent(); ->assertNoContent();

View File

@ -106,7 +106,7 @@ class MigrationTestData
"name": "' . OtpTestData::ACCOUNT . '", "name": "' . OtpTestData::ACCOUNT . '",
"issuer": "' . OtpTestData::SERVICE . '", "issuer": "' . OtpTestData::SERVICE . '",
"note": "", "note": "",
"icon": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPg0KICAgPGNpcmNsZSBjeD0iNTEyIiBjeT0iNTEyIiByPSI1MTIiIHN0eWxlPSJmaWxsOiMwMDBlOWMiLz4NCiAgIDxwYXRoIGQ9Im03MDAuMiA0NjYuNSA2MS4yLTEwNi4zYzIzLjYgNDEuNiAzNy4yIDg5LjggMzcuMiAxNDEuMSAwIDY4LjgtMjQuMyAxMzEuOS02NC43IDE4MS40SDU3NS44bDQ4LjctODQuNmgtNjQuNGw3NS44LTEzMS43IDY0LjMuMXptLTU1LjQtMTI1LjJMNDQ4LjMgNjgyLjVsLjEuMkgyOTAuMWMtNDAuNS00OS41LTY0LjctMTEyLjYtNjQuNy0xODEuNCAwLTUxLjQgMTMuNi05OS42IDM3LjMtMTQxLjNsMTAyLjUgMTc4LjIgMTEzLjMtMTk3aDE2Ni4zeiIgc3R5bGU9ImZpbGw6I2ZmZiIvPg0KPC9zdmc+DQo=", "icon": "' . OtpTestData::ICON_SVG_DATA_ENCODED . '",
"icon_mime": "image\/svg+xml", "icon_mime": "image\/svg+xml",
"info": { "info": {
"secret": "' . OtpTestData::SECRET . '", "secret": "' . OtpTestData::SECRET . '",
@ -463,4 +463,249 @@ class MigrationTestData
}, },
"db": "1rX0ajzsxNbhN2hvnNCMBNooLlzqwz\/LMT3bNEIJjPH+zIvIbA6GVVPHLpna+yvjxLPKVkt1OQig==" "db": "1rX0ajzsxNbhN2hvnNCMBNooLlzqwz\/LMT3bNEIJjPH+zIvIbA6GVVPHLpna+yvjxLPKVkt1OQig=="
}'; }';
const VALID_2FAUTH_JSON_MIGRATION_PAYLOAD = '
{
"app": "2fauth_v3.4.1",
"schema": 1,
"datetime": "2022-12-14T14:53:06.173939Z",
"data":
[
{
"otp_type": "totp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::SERVICE . '",
"icon": null,
"icon_mime": null,
"icon_file": null,
"secret": "' . OtpTestData::SECRET . '",
"digits": ' . OtpTestData::DIGITS_CUSTOM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": ' . OtpTestData::PERIOD_CUSTOM . ',
"counter": null,
"legacy_uri": "' . OtpTestData::TOTP_FULL_CUSTOM_URI_NO_IMG . '"
},
{
"otp_type": "hotp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::SERVICE . '",
"icon": null,
"icon_mime": null,
"icon_file": null,
"secret": "' . OtpTestData::SECRET . '",
"digits": ' . OtpTestData::DIGITS_CUSTOM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": null,
"counter": ' . OtpTestData::COUNTER_CUSTOM . ',
"legacy_uri": "' . OtpTestData::HOTP_FULL_CUSTOM_URI_NO_IMG . '"
},
{
"otp_type": "steamtotp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::STEAM . '",
"icon": null,
"icon_mime": null,
"icon_file": null,
"secret": "' . OtpTestData::STEAM_SECRET . '",
"digits": ' . OtpTestData::DIGITS_STEAM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": ' . OtpTestData::PERIOD_CUSTOM . ',
"counter": null,
"legacy_uri": "' . OtpTestData::STEAM_TOTP_URI . '"
}
]
}';
const VALID_2FAUTH_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_OTP_TYPE = '
{
"app": "2fauth_v3.4.1",
"schema": 1,
"datetime": "2022-12-14T14:53:06.173939Z",
"data":
[
{
"otp_type": "totp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::SERVICE . '",
"icon": null,
"icon_mime": null,
"icon_file": null,
"secret": "' . OtpTestData::SECRET . '",
"digits": ' . OtpTestData::DIGITS_CUSTOM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": ' . OtpTestData::PERIOD_CUSTOM . ',
"counter": null,
"legacy_uri": "' . OtpTestData::TOTP_FULL_CUSTOM_URI_NO_IMG . '"
},
{
"otp_type": "Xotp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::SERVICE . '",
"icon": null,
"icon_mime": null,
"icon_file": null,
"secret": "' . OtpTestData::SECRET . '",
"digits": ' . OtpTestData::DIGITS_CUSTOM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": ' . OtpTestData::PERIOD_CUSTOM . ',
"counter": null,
"legacy_uri": "' . OtpTestData::TOTP_FULL_CUSTOM_URI_NO_IMG . '"
}
]
}';
const VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_SVG_ICON = '
{
"app": "2fauth_v3.4.1",
"schema": 1,
"datetime": "2022-12-14T14:53:06.173939Z",
"data":
[
{
"otp_type": "totp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::SERVICE . '",
"icon": "' . OtpTestData::ICON_SVG . '",
"icon_mime": "image\/svg+xml",
"icon_file": "' . OtpTestData::ICON_SVG_DATA_ENCODED . '",
"secret": "' . OtpTestData::SECRET . '",
"digits": ' . OtpTestData::DIGITS_CUSTOM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": ' . OtpTestData::PERIOD_CUSTOM . ',
"counter": null,
"legacy_uri": "' . OtpTestData::TOTP_FULL_CUSTOM_URI_NO_IMG . '"
}
]
}';
const VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_JPG_ICON = '
{
"app": "2fauth_v3.4.1",
"schema": 1,
"datetime": "2022-12-14T14:53:06.173939Z",
"data":
[
{
"otp_type": "totp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::SERVICE . '",
"icon": "' . OtpTestData::ICON_JPEG . '",
"icon_mime": "image\/svg+xml",
"icon_file": "' . OtpTestData::ICON_JPEG_DATA . '",
"secret": "' . OtpTestData::SECRET . '",
"digits": ' . OtpTestData::DIGITS_CUSTOM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": ' . OtpTestData::PERIOD_CUSTOM . ',
"counter": null,
"legacy_uri": "' . OtpTestData::TOTP_FULL_CUSTOM_URI_NO_IMG . '"
}
]
}';
const VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_PNG_ICON = '
{
"app": "2fauth_v3.4.1",
"schema": 1,
"datetime": "2022-12-14T14:53:06.173939Z",
"data":
[
{
"otp_type": "totp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::SERVICE . '",
"icon": "' . OtpTestData::ICON_PNG . '",
"icon_mime": "image\/svg+xml",
"icon_file": "' . OtpTestData::ICON_PNG_DATA . '",
"secret": "' . OtpTestData::SECRET . '",
"digits": ' . OtpTestData::DIGITS_CUSTOM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": ' . OtpTestData::PERIOD_CUSTOM . ',
"counter": null,
"legacy_uri": "' . OtpTestData::TOTP_FULL_CUSTOM_URI_NO_IMG . '"
}
]
}';
const VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_BMP_ICON = '
{
"app": "2fauth_v3.4.1",
"schema": 1,
"datetime": "2022-12-14T14:53:06.173939Z",
"data":
[
{
"otp_type": "totp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::SERVICE . '",
"icon": "' . OtpTestData::ICON_BMP . '",
"icon_mime": "image\/svg+xml",
"icon_file": "' . OtpTestData::ICON_BMP_DATA . '",
"secret": "' . OtpTestData::SECRET . '",
"digits": ' . OtpTestData::DIGITS_CUSTOM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": ' . OtpTestData::PERIOD_CUSTOM . ',
"counter": null,
"legacy_uri": "' . OtpTestData::TOTP_FULL_CUSTOM_URI_NO_IMG . '"
}
]
}';
const VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_WEBP_ICON = '
{
"app": "2fauth_v3.4.1",
"schema": 1,
"datetime": "2022-12-14T14:53:06.173939Z",
"data":
[
{
"otp_type": "totp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::SERVICE . '",
"icon": "' . OtpTestData::ICON_WEBP . '",
"icon_mime": "image\/svg+xml",
"icon_file": "' . OtpTestData::ICON_WEBP_DATA . '",
"secret": "' . OtpTestData::SECRET . '",
"digits": ' . OtpTestData::DIGITS_CUSTOM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": ' . OtpTestData::PERIOD_CUSTOM . ',
"counter": null,
"legacy_uri": "' . OtpTestData::TOTP_FULL_CUSTOM_URI_NO_IMG . '"
}
]
}';
const VALID_2FAUTH_JSON_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_ICON = '
{
"app": "2fauth_v3.4.1",
"schema": 1,
"datetime": "2022-12-14T14:53:06.173939Z",
"data":
[
{
"otp_type": "totp",
"account": "' . OtpTestData::ACCOUNT . '",
"service": "' . OtpTestData::SERVICE . '",
"icon": "' . OtpTestData::ICON_PNG . '",
"icon_mime": "image\/gif",
"icon_file": "' . OtpTestData::ICON_PNG_DATA . '",
"secret": "' . OtpTestData::SECRET . '",
"digits": ' . OtpTestData::DIGITS_CUSTOM . ',
"algorithm": "' . OtpTestData::ALGORITHM_CUSTOM . '",
"period": ' . OtpTestData::PERIOD_CUSTOM . ',
"counter": null,
"legacy_uri": "' . OtpTestData::TOTP_FULL_CUSTOM_URI_NO_IMG . '"
}
]
}';
const INVALID_2FAUTH_JSON_MIGRATION_PAYLOAD = '
{
"app": "2fauth_v3.4.1",
"schema": 1,
"datetime": "2022-12-14T14:53:06.173939Z",
"data":
[
,
]
}';
} }

View File

@ -54,6 +54,8 @@ class OtpTestData
const ICON_SVG_DATA = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><circle cx="512" cy="512" r="512" style="fill:#000e9c"/><path d="m700.2 466.5 61.2-106.3c23.6 41.6 37.2 89.8 37.2 141.1 0 68.8-24.3 131.9-64.7 181.4H575.8l48.7-84.6h-64.4l75.8-131.7 64.3.1zm-55.4-125.2L448.3 682.5l.1.2H290.1c-40.5-49.5-64.7-112.6-64.7-181.4 0-51.4 13.6-99.6 37.3-141.3l102.5 178.2 113.3-197h166.3z" style="fill:#fff"/></svg>'; const ICON_SVG_DATA = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><circle cx="512" cy="512" r="512" style="fill:#000e9c"/><path d="m700.2 466.5 61.2-106.3c23.6 41.6 37.2 89.8 37.2 141.1 0 68.8-24.3 131.9-64.7 181.4H575.8l48.7-84.6h-64.4l75.8-131.7 64.3.1zm-55.4-125.2L448.3 682.5l.1.2H290.1c-40.5-49.5-64.7-112.6-64.7-181.4 0-51.4 13.6-99.6 37.3-141.3l102.5 178.2 113.3-197h166.3z" style="fill:#fff"/></svg>';
const ICON_SVG_DATA_ENCODED = 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPg0KICAgPGNpcmNsZSBjeD0iNTEyIiBjeT0iNTEyIiByPSI1MTIiIHN0eWxlPSJmaWxsOiMwMDBlOWMiLz4NCiAgIDxwYXRoIGQ9Im03MDAuMiA0NjYuNSA2MS4yLTEwNi4zYzIzLjYgNDEuNiAzNy4yIDg5LjggMzcuMiAxNDEuMSAwIDY4LjgtMjQuMyAxMzEuOS02NC43IDE4MS40SDU3NS44bDQ4LjctODQuNmgtNjQuNGw3NS44LTEzMS43IDY0LjMuMXptLTU1LjQtMTI1LjJMNDQ4LjMgNjgyLjVsLjEuMkgyOTAuMWMtNDAuNS00OS41LTY0LjctMTEyLjYtNjQuNy0xODEuNCAwLTUxLjQgMTMuNi05OS42IDM3LjMtMTQxLjNsMTAyLjUgMTc4LjIgMTEzLjMtMTk3aDE2Ni4zeiIgc3R5bGU9ImZpbGw6I2ZmZiIvPg0KPC9zdmc+DQo=';
const TOTP_FULL_CUSTOM_URI_NO_IMG = 'otpauth://totp/' . self::SERVICE . ':' . self::ACCOUNT . '?secret=' . self::SECRET . '&issuer=' . self::SERVICE . '&digits=' . self::DIGITS_CUSTOM . '&period=' . self::PERIOD_CUSTOM . '&algorithm=' . self::ALGORITHM_CUSTOM; const TOTP_FULL_CUSTOM_URI_NO_IMG = 'otpauth://totp/' . self::SERVICE . ':' . self::ACCOUNT . '?secret=' . self::SECRET . '&issuer=' . self::SERVICE . '&digits=' . self::DIGITS_CUSTOM . '&period=' . self::PERIOD_CUSTOM . '&algorithm=' . self::ALGORITHM_CUSTOM;
const TOTP_FULL_CUSTOM_URI = self::TOTP_FULL_CUSTOM_URI_NO_IMG . '&image=' . self::IMAGE; const TOTP_FULL_CUSTOM_URI = self::TOTP_FULL_CUSTOM_URI_NO_IMG . '&image=' . self::IMAGE;

View File

@ -7,6 +7,7 @@ use Tests\FeatureTestCase;
/** /**
* @covers \App\Http\Controllers\Auth\RegisterController * @covers \App\Http\Controllers\Auth\RegisterController
* @covers \App\Http\Requests\UserStoreRequest
*/ */
class RegisterControllerTest extends FeatureTestCase class RegisterControllerTest extends FeatureTestCase
{ {

View File

@ -2,7 +2,6 @@
namespace Tests\Feature\Http\Auth; namespace Tests\Feature\Http\Auth;
use App\Facades\Settings;
use App\Models\Group; use App\Models\Group;
use App\Models\TwoFAccount; use App\Models\TwoFAccount;
use App\Models\User; use App\Models\User;
@ -12,6 +11,7 @@ use Tests\FeatureTestCase;
/** /**
* @covers \App\Http\Controllers\Auth\UserController * @covers \App\Http\Controllers\Auth\UserController
* @covers \App\Http\Middleware\RejectIfDemoMode * @covers \App\Http\Middleware\RejectIfDemoMode
* @covers \App\Http\Requests\UserUpdateRequest
*/ */
class UserControllerTest extends FeatureTestCase class UserControllerTest extends FeatureTestCase
{ {
@ -63,6 +63,33 @@ class UserControllerTest extends FeatureTestCase
]); ]);
} }
/**
* @test
*/
public function test_update_user_with_uppercased_email_returns_success()
{
$response = $this->actingAs($this->user, 'web-guard')
->json('PUT', '/user', [
'name' => self::NEW_USERNAME,
'email' => strtoupper(self::NEW_EMAIL),
'password' => self::PASSWORD,
])
->assertOk()
->assertExactJson([
'name' => self::NEW_USERNAME,
'id' => $this->user->id,
'email' => self::NEW_EMAIL,
'is_admin' => false,
]);
$this->assertDatabaseHas('users', [
'name' => self::NEW_USERNAME,
'id' => $this->user->id,
'email' => self::NEW_EMAIL,
'is_admin' => false,
]);
}
/** /**
* @test * @test
*/ */

View File

@ -12,6 +12,7 @@ use Tests\FeatureTestCase;
/** /**
* @covers \App\Http\Controllers\Auth\WebAuthnLoginController * @covers \App\Http\Controllers\Auth\WebAuthnLoginController
* @covers \App\Models\User * @covers \App\Models\User
* @covers \App\Extensions\WebauthnTwoFAuthUserProvider
*/ */
class WebAuthnLoginControllerTest extends FeatureTestCase class WebAuthnLoginControllerTest extends FeatureTestCase
{ {
@ -175,6 +176,26 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
]); ]);
} }
/**
* @test
*/
public function test_legacy_login_is_rejected_when_webauthn_only_is_enable()
{
$this->user = User::factory()->create([
'email' => self::EMAIL,
]);
// Set to webauthn only
$this->user['preferences->useWebauthnOnly'] = true;
$this->user->save();
$response = $this->json('POST', '/user/login', [
'email' => self::EMAIL,
'password' => 'password',
])
->assertUnauthorized();
}
/** /**
* @test * @test
* *
@ -289,7 +310,7 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
false, false,
)]); )]);
for ($i=0; $i < $throttle - 1; $i++) { for ($i = 0; $i < $throttle - 1; $i++) {
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID); $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
} }

View File

@ -0,0 +1,54 @@
<?php
namespace Tests\Feature\Http\Middlewares;
use App\Http\Middleware\AdminOnly;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request;
use Tests\FeatureTestCase;
class AdminOnlyMiddlewareTest extends FeatureTestCase
{
/**
* @test
*/
public function test_users_are_rejected()
{
$this->expectException(AuthorizationException::class);
/**
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
*/
$user = User::factory()->create();
$this->actingAs($user);
$request = Request::create('/admin', 'GET');
$middleware = new AdminOnly;
$response = $middleware->handle($request, function () {
});
}
/**
* @test
*/
public function test_admins_pass()
{
/**
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
*/
$admin = User::factory()->administrator()->create();
$this->actingAs($admin);
$request = Request::create('/admin', 'GET');
$middleware = new AdminOnly;
$response = $middleware->handle($request, function () {
});
$this->assertNull($response);
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Tests\Feature\Http\Requests;
use App\Http\Requests\WebauthnAssertedRequest;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
/**
* @covers \App\Http\Requests\WebauthnAssertedRequest
*/
class WebauthnAssertedRequestTest extends TestCase
{
use WithoutMiddleware;
/**
* @dataProvider provideValidData
*/
public function test_valid_data(array $data) : void
{
$request = new WebauthnAssertedRequest();
$validator = Validator::make($data, $request->rules());
$this->assertFalse($validator->fails());
}
/**
* Provide Valid data for validation test
*/
public function provideValidData() : array
{
return [
[[
'id' => 'string',
'rawId' => 'string',
'type' => 'string',
'response' => [
'clientDataJSON' => 'string',
'authenticatorData' => 'string',
'signature' => 'string',
'userHandle' => null,
],
'email' => 'valid@email.com',
]],
];
}
/**
* @dataProvider provideInvalidData
*/
public function test_invalid_data(array $data) : void
{
$request = new WebauthnAssertedRequest();
$validator = Validator::make($data, $request->rules());
$this->assertTrue($validator->fails());
}
/**
* Provide invalid data for validation test
*/
public function provideInvalidData() : array
{
return [
[[
'email' => '', // required
]],
[[
'email' => true, // email
]],
[[
'email' => 0, // email
]],
[[
'email' => 'sdfsdf@', // email
]],
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Tests\Feature\Models;
use App\Models\User;
use Tests\FeatureTestCase;
/**
* @covers \App\Models\User
*/
class UserModelTest extends FeatureTestCase
{
/**
* @test
*/
public function test_admin_scope_returns_only_admin()
{
User::factory()->count(4)->create();
$firstAdmin = User::factory()->administrator()->create([
'name' => 'first',
]);
$secondAdmin = User::factory()->administrator()->create([
'name' => 'secondAdmin',
]);
$admins = User::admins()->get();
$this->assertCount(2, $admins);
$this->assertEquals($admins[0]->is_admin, true);
$this->assertEquals($admins[1]->is_admin, true);
$this->assertEquals($admins[0]->name, $firstAdmin->name);
$this->assertEquals($admins[1]->name, $secondAdmin->name);
}
}

View File

@ -174,4 +174,29 @@ class HandlerTest extends TestCase
'message', 'message',
]); ]);
} }
/**
* @test
*/
public function test_AccessDeniedException_returns_forbidden_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(\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException::class);
$response = $method->invokeArgs($instance, [$request, $mockException]);
$this->assertInstanceOf(JsonResponse::class, $response);
$response = \Illuminate\Testing\TestResponse::fromBaseResponse($response);
$response->assertStatus(403)
->assertJsonStructure([
'message',
]);
}
} }

View File

@ -135,4 +135,98 @@ class HelpersTest extends TestCase
], ],
]; ];
} }
/**
* @test
*
* @dataProvider commaSeparatedToArrayProvider
*/
public function test_commaSeparatedToArray_returns_ids_in_array($str, $expected)
{
$array = Helpers::commaSeparatedToArray($str);
$this->assertEquals($expected, $array);
}
/**
* Provide data for cleanVersionNumber() tests
*/
public function commaSeparatedToArrayProvider()
{
return [
'NOMINAL' => [
'1,2,3',
[1, 2, 3],
],
'DUPLICATE' => [
'1,2,2,3',
[1, 2, 2, 3],
],
];
}
/**
* @test
*
* @dataProvider invalidCommaSeparatedToArrayProvider
*/
public function test_commaSeparatedToArray_returns_unchanged_ids($str, $expected)
{
$array = Helpers::commaSeparatedToArray($str);
$this->assertEquals($expected, $array);
}
/**
* Provide data for cleanVersionNumber() tests
*/
public function invalidCommaSeparatedToArrayProvider()
{
return [
'INVALID_IDS_LEADING_SPACES' => [
'1, 2,3',
'1, 2,3',
],
'INVALID_IDS_TRAILING_SPACES' => [
'1,2 ,3',
'1,2 ,3',
],
'INVALID_IDS_BAD_SEPARATOR' => [
'1/2/3',
'1/2/3',
],
'INVALID_IDS_NOT_DIGIT' => [
'a,b,c',
'a,b,c',
],
'INVALID_IDS_MISSING_DIGIT' => [
'1,,3',
'1,,3',
],
'INVALID_IDS_LEADING_COMMA' => [
',2,3',
',2,3',
],
'INVALID_IDS_TRAILING_COMMA' => [
'1,2,',
'1,2,',
],
'NOT_STRING_BOOLEAN' => [
true,
true,
],
'NOT_STRING_INT' => [
1,
1,
],
'NOT_STRING_ARRAY' => [
[1],
[1],
],
'NOT_STRING_NULL' => [
null,
null,
],
];
}
} }

View File

@ -13,6 +13,7 @@ use App\Services\Migrators\GoogleAuthMigrator;
use App\Services\Migrators\Migrator; use App\Services\Migrators\Migrator;
use App\Services\Migrators\PlainTextMigrator; use App\Services\Migrators\PlainTextMigrator;
use App\Services\Migrators\TwoFASMigrator; use App\Services\Migrators\TwoFASMigrator;
use App\Services\Migrators\TwoFAuthMigrator;
use App\Services\SettingService; use App\Services\SettingService;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Mockery; use Mockery;
@ -30,6 +31,7 @@ use Tests\TestCase;
* @covers \App\Services\Migrators\TwoFASMigrator * @covers \App\Services\Migrators\TwoFASMigrator
* @covers \App\Services\Migrators\PlainTextMigrator * @covers \App\Services\Migrators\PlainTextMigrator
* @covers \App\Services\Migrators\GoogleAuthMigrator * @covers \App\Services\Migrators\GoogleAuthMigrator
* @covers \App\Services\Migrators\TwoFAuthMigrator
* *
* @uses \App\Models\TwoFAccount * @uses \App\Models\TwoFAccount
*/ */
@ -208,6 +210,12 @@ class MigratorTest extends TestCase
'gauth', 'gauth',
$hasSteam = false, $hasSteam = false,
], ],
'2FAUTH_MIGRATION_PAYLOAD' => [
new TwoFAuthMigrator(),
MigrationTestData::VALID_2FAUTH_JSON_MIGRATION_PAYLOAD,
'custom',
$hasSteam = true,
],
]; ];
} }
@ -273,6 +281,10 @@ class MigratorTest extends TestCase
new GoogleAuthMigrator(), new GoogleAuthMigrator(),
MigrationTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA, MigrationTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA,
], ],
'INVALID_2FAUTH_JSON_MIGRATION_PAYLOAD' => [
new TwoFAuthMigrator(),
MigrationTestData::INVALID_2FAUTH_JSON_MIGRATION_PAYLOAD,
],
]; ];
} }
@ -313,6 +325,10 @@ class MigratorTest extends TestCase
new TwoFASMigrator(), new TwoFASMigrator(),
MigrationTestData::VALID_2FAS_MIGRATION_PAYLOAD_WITH_UNSUPPORTED_OTP_TYPE, 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,
],
]; ];
} }
@ -394,12 +410,71 @@ class MigratorTest extends TestCase
Storage::disk('icons')->assertDirectoryEmpty('/'); 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 * @test
* *
* @dataProvider factoryProvider * @dataProvider factoryProvider
*/ */
public function test_factory_returns_plain_text_migrator($payload, $migratorClass) public function test_factory_returns_relevant_migrator($payload, $migratorClass)
{ {
$factory = new MigratorFactory(); $factory = new MigratorFactory();
@ -434,6 +509,10 @@ class MigratorTest extends TestCase
MigrationTestData::GOOGLE_AUTH_MIGRATION_URI, MigrationTestData::GOOGLE_AUTH_MIGRATION_URI,
GoogleAuthMigrator::class, GoogleAuthMigrator::class,
], ],
'2FAUTH_MIGRATION_URI' => [
MigrationTestData::VALID_2FAUTH_JSON_MIGRATION_PAYLOAD,
TwoFAuthMigrator::class,
],
]; ];
} }