From 8b0871e8bad0067c3f266ece2acabf8a114eb566 Mon Sep 17 00:00:00 2001 From: Bubka <858858+Bubka@users.noreply.github.com> Date: Tue, 30 Nov 2021 17:39:33 +0100 Subject: [PATCH] Complete Unit, Feature and Api tests --- .../Maintenance/FixUnsplittedAccounts.php | 3 + app/Group.php | 2 + app/Services/TwoFAccountService.php | 29 +- .../Auth/ForgotPasswordControllerTest.php | 2 +- .../v1/Controllers/GroupControllerTest.php | 1 + .../Api/v1/Controllers/IconControllerTest.php | 6 +- .../v1/Controllers/SettingControllerTest.php | 3 - .../Controllers/TwoFAccountControllerTest.php | 44 +-- .../TwoFAccountDynamicRequestTest.php | 55 ++++ .../Feature/Console/CheckDbConnectionTest.php | 39 +++ .../ResetDemoTest.php} | 4 +- .../Http/Requests/LoginRequestTest.php | 109 ++++++ tests/Feature/Services/GroupServiceTest.php | 310 ++++++++++++++++++ tests/Feature/Services/QrCodeServiceTest.php | 67 ++++ ...ServiceTest.php => SettingServiceTest.php} | 143 +++++++- .../Services/TwoFAccountServiceTest.php | 143 +++++++- tests/ModelTestCase.php | 138 ++++++++ .../v1/Controllers/GroupControllerTest.php | 186 +++++++++++ tests/Unit/Events/GroupDeletingTest.php | 25 ++ tests/Unit/Events/TwoFAccountDeletedTest.php | 29 ++ tests/Unit/Exceptions/HandlerTest.php | 103 ++++++ tests/Unit/GroupModelTest.php | 43 +++ tests/Unit/Listeners/CleanIconStorageTest.php | 33 ++ .../DissociateTwofaccountFromGroupTest.php | 26 ++ tests/Unit/TwoFAccountModelTest.php | 111 +++++++ tests/Unit/UserModelTest.php | 39 +++ 26 files changed, 1646 insertions(+), 47 deletions(-) create mode 100644 tests/Api/v1/Requests/TwoFAccountDynamicRequestTest.php create mode 100644 tests/Feature/Console/CheckDbConnectionTest.php rename tests/Feature/{ConsoleTest.php => Console/ResetDemoTest.php} (98%) create mode 100644 tests/Feature/Http/Requests/LoginRequestTest.php create mode 100644 tests/Feature/Services/GroupServiceTest.php create mode 100644 tests/Feature/Services/QrCodeServiceTest.php rename tests/Feature/Services/{AppstractOptionsServiceTest.php => SettingServiceTest.php} (50%) create mode 100644 tests/ModelTestCase.php create mode 100644 tests/Unit/Api/v1/Controllers/GroupControllerTest.php create mode 100644 tests/Unit/Events/GroupDeletingTest.php create mode 100644 tests/Unit/Events/TwoFAccountDeletedTest.php create mode 100644 tests/Unit/Exceptions/HandlerTest.php create mode 100644 tests/Unit/GroupModelTest.php create mode 100644 tests/Unit/Listeners/CleanIconStorageTest.php create mode 100644 tests/Unit/Listeners/DissociateTwofaccountFromGroupTest.php create mode 100644 tests/Unit/TwoFAccountModelTest.php create mode 100644 tests/Unit/UserModelTest.php diff --git a/app/Console/Commands/Maintenance/FixUnsplittedAccounts.php b/app/Console/Commands/Maintenance/FixUnsplittedAccounts.php index bee6859b..d4976128 100644 --- a/app/Console/Commands/Maintenance/FixUnsplittedAccounts.php +++ b/app/Console/Commands/Maintenance/FixUnsplittedAccounts.php @@ -9,6 +9,9 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Log; +/** + * @codeCoverageIgnore + */ class FixUnsplittedAccounts extends Command { /** diff --git a/app/Group.php b/app/Group.php index c0ba2f80..3893f5a6 100644 --- a/app/Group.php +++ b/app/Group.php @@ -63,7 +63,9 @@ protected static function boot() parent::boot(); static::deleted(function ($model) { + // @codeCoverageIgnoreStart Log::info(sprintf('Group %s deleted', var_export($model->name, true))); + // @codeCoverageIgnoreEnd }); } diff --git a/app/Services/TwoFAccountService.php b/app/Services/TwoFAccountService.php index c0a55f0d..53135289 100644 --- a/app/Services/TwoFAccountService.php +++ b/app/Services/TwoFAccountService.php @@ -200,17 +200,12 @@ public function withdraw($ids) : void // whereIn() expects an array $ids = is_array($ids) ? $ids : func_get_args(); - if ($ids) { - TwoFAccount::whereIn('id', $ids) - ->update( - ['group_id' => NULL] - ); - - Log::info(sprintf('TwoFAccounts #%s withdrawn', implode(',#', $ids))); - } - // @codeCoverageIgnoreStart - else Log::info('No TwoFAccount to withdraw'); - // @codeCoverageIgnoreEnd + TwoFAccount::whereIn('id', $ids) + ->update( + ['group_id' => NULL] + ); + + Log::info(sprintf('TwoFAccounts #%s withdrawn', implode(',#', $ids))); } @@ -242,10 +237,14 @@ public function delete($ids) : int */ private function commaSeparatedToArray($ids) { - $regex = "/^\d+(,{1}\d+)*$/"; - if (preg_match($regex, $ids)) { - $ids = explode(',', $ids); + if(is_string($ids)) + { + $regex = "/^\d+(,{1}\d+)*$/"; + if (preg_match($regex, $ids)) { + $ids = explode(',', $ids); + } } + return $ids; } @@ -467,8 +466,10 @@ private function storeTokenImageAsIcon() return $newFilename; } + // @codeCoverageIgnoreStart catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) { return null; } + // @codeCoverageIgnoreEnd } } \ No newline at end of file diff --git a/tests/Api/v1/Controllers/Auth/ForgotPasswordControllerTest.php b/tests/Api/v1/Controllers/Auth/ForgotPasswordControllerTest.php index eec1e11d..c0605707 100644 --- a/tests/Api/v1/Controllers/Auth/ForgotPasswordControllerTest.php +++ b/tests/Api/v1/Controllers/Auth/ForgotPasswordControllerTest.php @@ -81,7 +81,7 @@ public function test_submit_email_password_request_returns_success() /** * @test */ - public function test_submit_email_password_request__in_demo_mode_returns_unauthorized() + public function test_submit_email_password_request_in_demo_mode_returns_unauthorized() { Config::set('2fauth.config.isDemoApp', true); diff --git a/tests/Api/v1/Controllers/GroupControllerTest.php b/tests/Api/v1/Controllers/GroupControllerTest.php index d6054d16..ab20d52b 100644 --- a/tests/Api/v1/Controllers/GroupControllerTest.php +++ b/tests/Api/v1/Controllers/GroupControllerTest.php @@ -10,6 +10,7 @@ /** * @covers \App\Api\v1\Controllers\GroupController + * @covers \App\Api\v1\Resources\GroupResource */ class GroupControllerTest extends FeatureTestCase { diff --git a/tests/Api/v1/Controllers/IconControllerTest.php b/tests/Api/v1/Controllers/IconControllerTest.php index 31239220..dd52a83d 100644 --- a/tests/Api/v1/Controllers/IconControllerTest.php +++ b/tests/Api/v1/Controllers/IconControllerTest.php @@ -4,13 +4,14 @@ use Illuminate\Http\UploadedFile; use Illuminate\Foundation\Testing\WithoutMiddleware; -use Tests\TestCase; +use Tests\FeatureTestCase; +use App\TwoFAccount; /** * @covers \App\Api\v1\Controllers\IconController */ -class IconControllerTest extends TestCase +class IconControllerTest extends FeatureTestCase { use WithoutMiddleware; @@ -52,7 +53,6 @@ public function test_delete_icon_returns_success() { $response = $this->json('DELETE', '/api/v1/icons/testIcon.jpg') ->assertNoContent(204); - } diff --git a/tests/Api/v1/Controllers/SettingControllerTest.php b/tests/Api/v1/Controllers/SettingControllerTest.php index 119404a8..49186256 100644 --- a/tests/Api/v1/Controllers/SettingControllerTest.php +++ b/tests/Api/v1/Controllers/SettingControllerTest.php @@ -172,7 +172,6 @@ public function test_update_unchanged_native_setting_returns_updated_setting() { $response = $this->actingAs($this->user, 'api') ->json('PUT', '/api/v1/settings/' . self::TWOFAUTH_NATIVE_SETTING, [ - 'key' => self::TWOFAUTH_NATIVE_SETTING, 'value' => self::TWOFAUTH_NATIVE_SETTING_CHANGED_VALUE, ]) ->assertOk() @@ -193,7 +192,6 @@ public function test_update_custom_user_setting_returns_updated_setting() $response = $this->actingAs($this->user, 'api') ->json('PUT', '/api/v1/settings/' . self::USER_DEFINED_SETTING, [ - 'key' => self::USER_DEFINED_SETTING, 'value' => self::USER_DEFINED_SETTING_CHANGED_VALUE, ]) ->assertOk() @@ -211,7 +209,6 @@ public function test_update_missing_user_setting_returns_created_setting() { $response = $this->actingAs($this->user, 'api') ->json('PUT', '/api/v1/settings/' . self::USER_DEFINED_SETTING, [ - 'key' => self::USER_DEFINED_SETTING, 'value' => self::USER_DEFINED_SETTING_CHANGED_VALUE, ]) ->assertOk() diff --git a/tests/Api/v1/Controllers/TwoFAccountControllerTest.php b/tests/Api/v1/Controllers/TwoFAccountControllerTest.php index 74acfb6a..2baf251a 100644 --- a/tests/Api/v1/Controllers/TwoFAccountControllerTest.php +++ b/tests/Api/v1/Controllers/TwoFAccountControllerTest.php @@ -1,6 +1,6 @@ setTo(true); + // public function test_show_twofaccount_with_indeciphered_data_returns_replaced_data() + // { + // $dbEncryptionService = resolve('App\Services\DbEncryptionService'); + // $dbEncryptionService->setTo(true); - $twofaccount = factory(TwoFAccount::class)->create(); + // $twofaccount = factory(TwoFAccount::class)->create(); - DB::table('twofaccounts') - ->where('id', $twofaccount->id) - ->update([ - 'secret' => '**encrypted**', - 'account' => '**encrypted**', - ]); + // DB::table('twofaccounts') + // ->where('id', $twofaccount->id) + // ->update([ + // 'secret' => '**encrypted**', + // 'account' => '**encrypted**', + // ]); - $response = $this->actingAs($this->user, 'api') - ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id) - ->assertJsonFragment([ - 'secret' => '*indecipherable*', - 'account' => '*indecipherable*', - ]); - } + // $response = $this->actingAs($this->user, 'api') + // ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id) + // ->assertJsonFragment([ + // 'secret' => '*indecipherable*', + // 'account' => '*indecipherable*', + // ]); + // } /** @@ -784,8 +786,8 @@ public function test_get_otp_by_posting_multiple_inputs_returns_bad_request() */ public function test_get_otp_using_indecipherable_twofaccount_id_returns_bad_request() { - $dbEncryptionService = resolve('App\Services\DbEncryptionService'); - $dbEncryptionService->setTo(true); + $settingService = resolve('App\Services\SettingServiceInterface'); + $settingService->set('useEncryption', true); $twofaccount = factory(TwoFAccount::class)->create(); diff --git a/tests/Api/v1/Requests/TwoFAccountDynamicRequestTest.php b/tests/Api/v1/Requests/TwoFAccountDynamicRequestTest.php new file mode 100644 index 00000000..e7ea534f --- /dev/null +++ b/tests/Api/v1/Requests/TwoFAccountDynamicRequestTest.php @@ -0,0 +1,55 @@ +once() + ->andReturn(true); + + $request = new TwoFAccountDynamicRequest(); + + $this->assertTrue($request->authorize()); + } + + /** + * @test + */ + public function test_returns_TwoFAccountUriRequest_rules_when_has_uri_input() + { + $twofaccountUriRequest = new TwoFAccountUriRequest(); + $request = new TwoFAccountDynamicRequest(); + $request->merge(['uri' => 'uristring']); + + $this->assertEquals($twofaccountUriRequest->rules(), $request->rules()); + } + + /** + * @test + */ + public function test_returns_TwoFAccountStoreRequest_rules_otherwise() + { + $twofaccountStoreRequest = new TwoFAccountStoreRequest(); + $request = new TwoFAccountDynamicRequest(); + + $this->assertEquals($twofaccountStoreRequest->rules(), $request->rules()); + } + +} \ No newline at end of file diff --git a/tests/Feature/Console/CheckDbConnectionTest.php b/tests/Feature/Console/CheckDbConnectionTest.php new file mode 100644 index 00000000..cbf3e6c7 --- /dev/null +++ b/tests/Feature/Console/CheckDbConnectionTest.php @@ -0,0 +1,39 @@ +artisan('2fauth:check-db-connection') + ->expectsOutput('This will return the name of the connected database, otherwise false') + ->expectsOutput(DB::connection()->getDatabaseName()) + ->assertExitCode(1); + } + + /** + * @test + */ + public function test_CheckDbConnection_without_db_returns_false() + { + DB::shouldReceive('connection', 'getPDO') + ->andThrow(new \Exception()); + + $this->artisan('2fauth:check-db-connection') + ->assertExitCode(0); + } +} \ No newline at end of file diff --git a/tests/Feature/ConsoleTest.php b/tests/Feature/Console/ResetDemoTest.php similarity index 98% rename from tests/Feature/ConsoleTest.php rename to tests/Feature/Console/ResetDemoTest.php index 45018027..ee1057e2 100644 --- a/tests/Feature/ConsoleTest.php +++ b/tests/Feature/Console/ResetDemoTest.php @@ -1,13 +1,13 @@ assertTrue($request->authorize()); + } + + + /** + * @dataProvider provideValidData + */ + public function test_valid_data(array $data) : void + { + factory(\App\User::class)->create([ + 'email' => 'JOHN.DOE@example.com' + ]); + + $request = new LoginRequest(); + $validator = Validator::make($data, $request->rules()); + + $this->assertFalse($validator->fails()); + } + + + /** + * Provide Valid data for validation test + */ + public function provideValidData() : array + { + return [ + [[ + 'email' => 'john.doe@example.com', + 'password' => 'MyPassword' + ]], + [[ + 'email' => 'JOHN.doe@example.com', + 'password' => 'MyPassword' + ]], + ]; + } + + + /** + * @dataProvider provideInvalidData + */ + public function test_invalid_data(array $data) : void + { + factory(\App\User::class)->create([ + 'email' => 'JOHN.DOE@example.com' + ]); + + $request = new LoginRequest(); + $validator = Validator::make($data, $request->rules()); + + $this->assertTrue($validator->fails()); + } + + + /** + * Provide invalid data for validation test + */ + public function provideInvalidData() : array + { + return [ + [[ + 'email' => '', // required + 'password' => 'MyPassword', + ]], + [[ + 'email' => 'john', // email + 'password' => 'MyPassword', + ]], + [[ + 'email' => 'john@example.com', // exists + 'password' => 'MyPassword', + ]], + [[ + 'email' => 'john.doe@example.com', + 'password' => '', // required + ]], + [[ + 'email' => 'john.doe@example.com', + 'password' => true, // string + ]], + ]; + } +} \ No newline at end of file diff --git a/tests/Feature/Services/GroupServiceTest.php b/tests/Feature/Services/GroupServiceTest.php new file mode 100644 index 00000000..d7c2c596 --- /dev/null +++ b/tests/Feature/Services/GroupServiceTest.php @@ -0,0 +1,310 @@ +groupService = $this->app->make('App\Services\GroupService'); + $this->settingService = $this->app->make('App\Services\SettingServiceInterface'); + + $this->groupOne = new Group; + $this->groupOne->name = 'MyGroupOne'; + $this->groupOne->save(); + + $this->groupTwo = new Group; + $this->groupTwo->name = 'MyGroupTwo'; + $this->groupTwo->save(); + + $this->twofaccountOne = new TwoFAccount; + $this->twofaccountOne->legacy_uri = self::TOTP_FULL_CUSTOM_URI; + $this->twofaccountOne->service = self::SERVICE; + $this->twofaccountOne->account = self::ACCOUNT; + $this->twofaccountOne->icon = self::ICON; + $this->twofaccountOne->otp_type = 'totp'; + $this->twofaccountOne->secret = self::SECRET; + $this->twofaccountOne->digits = self::DIGITS_CUSTOM; + $this->twofaccountOne->algorithm = self::ALGORITHM_CUSTOM; + $this->twofaccountOne->period = self::PERIOD_CUSTOM; + $this->twofaccountOne->counter = null; + $this->twofaccountOne->save(); + + $this->twofaccountTwo = new TwoFAccount; + $this->twofaccountTwo->legacy_uri = self::TOTP_FULL_CUSTOM_URI; + $this->twofaccountTwo->service = self::SERVICE; + $this->twofaccountTwo->account = self::ACCOUNT; + $this->twofaccountTwo->icon = self::ICON; + $this->twofaccountTwo->otp_type = 'totp'; + $this->twofaccountTwo->secret = self::SECRET; + $this->twofaccountTwo->digits = self::DIGITS_CUSTOM; + $this->twofaccountTwo->algorithm = self::ALGORITHM_CUSTOM; + $this->twofaccountTwo->period = self::PERIOD_CUSTOM; + $this->twofaccountTwo->counter = null; + $this->twofaccountTwo->save(); + } + + + /** + * @test + */ + public function test_getAll_returns_a_collection() + { + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Collection::class, $this->groupService->getAll()); + } + + + /** + * @test + */ + public function test_getAll_adds_pseudo_group_on_top_of_user_groups() + { + $groups = $this->groupService->getAll(); + + $this->assertEquals(0, $groups->first()->id); + $this->assertEquals(__('commons.all'), $groups->first()->name); + } + + + /** + * @test + */ + public function test_getAll_returns_pseudo_group_with_all_twofaccounts_count() + { + $groups = $this->groupService->getAll(); + + $this->assertEquals(self::TWOFACCOUNT_COUNT, $groups->first()->twofaccounts_count); + } + + + /** + * @test + */ + public function test_create_persists_and_returns_created_group() + { + $newGroup = $this->groupService->create(['name' => self::NEW_GROUP_NAME]); + + $this->assertDatabaseHas('groups', ['name' => self::NEW_GROUP_NAME]); + $this->assertInstanceOf(\App\Group::class, $newGroup); + $this->assertEquals(self::NEW_GROUP_NAME, $newGroup->name); + } + + + /** + * @test + */ + public function test_update_persists_and_returns_updated_group() + { + $this->groupOne = $this->groupService->update($this->groupOne, ['name' => self::NEW_GROUP_NAME]); + + $this->assertDatabaseHas('groups', ['name' => self::NEW_GROUP_NAME]); + $this->assertInstanceOf(\App\Group::class, $this->groupOne); + $this->assertEquals(self::NEW_GROUP_NAME, $this->groupOne->name); + } + + + /** + * @test + */ + public function test_delete_a_groupId_clear_db_and_returns_deleted_count() + { + $deleted = $this->groupService->delete($this->groupOne->id); + + $this->assertDatabaseMissing('groups', ['id' => $this->groupOne->id]); + $this->assertEquals(1, $deleted); + } + + + /** + * @test + */ + public function test_delete_an_array_of_ids_clear_db_and_returns_deleted_count() + { + $deleted = $this->groupService->delete([$this->groupOne->id, $this->groupTwo->id]); + + $this->assertDatabaseMissing('groups', ['id' => $this->groupOne->id]); + $this->assertDatabaseMissing('groups', ['id' => $this->groupTwo->id]); + $this->assertEquals(2, $deleted); + } + + + /** + * @test + */ + public function test_delete_default_group_reset_defaultGroup_setting() + { + $this->settingService->set('defaultGroup', $this->groupOne->id); + + $deleted = $this->groupService->delete($this->groupOne->id); + + $this->assertDatabaseHas('options', [ + 'key' => 'defaultGroup', + 'value' => 0 + ]); + } + + + /** + * @test + */ + public function test_delete_active_group_reset_activeGroup_setting() + { + $this->settingService->set('rememberActiveGroup', true); + $this->settingService->set('activeGroup', $this->groupOne->id); + + $deleted = $this->groupService->delete($this->groupOne->id); + + $this->assertDatabaseHas('options', [ + 'key' => 'activeGroup', + 'value' => 0 + ]); + } + + + /** + * @test + */ + public function test_assign_a_twofaccountid_to_a_specified_group_persists_the_relation() + { + + $this->groupService->assign($this->twofaccountOne->id, $this->groupOne); + + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->twofaccountOne->id, + 'group_id' => $this->groupOne->id, + ]); + } + + + /** + * @test + */ + public function test_assign_multiple_twofaccountid_to_a_specified_group_persists_the_relation() + { + $this->groupService->assign([$this->twofaccountOne->id, $this->twofaccountTwo->id], $this->groupOne); + + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->twofaccountOne->id, + 'group_id' => $this->groupOne->id, + ]); + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->twofaccountTwo->id, + 'group_id' => $this->groupOne->id, + ]); + } + + + /** + * @test + */ + public function test_assign_a_twofaccountid_to_no_group_assigns_to_default_group() + { + $this->settingService->set('defaultGroup', $this->groupTwo->id); + + $this->groupService->assign($this->twofaccountOne->id); + + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->twofaccountOne->id, + 'group_id' => $this->groupTwo->id, + ]); + } + + + /** + * @test + */ + public function test_assign_a_twofaccountid_to_no_group_assigns_to_active_group() + { + $this->settingService->set('defaultGroup', -1); + $this->settingService->set('activeGroup', $this->groupTwo->id); + + $this->groupService->assign($this->twofaccountOne->id); + + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->twofaccountOne->id, + 'group_id' => $this->groupTwo->id, + ]); + } + + + /** + * @test + */ + public function test_assign_a_twofaccountid_to_missing_active_group_does_not_fails() + { + $this->settingService->set('defaultGroup', -1); + $this->settingService->set('activeGroup', 100000); + + $this->groupService->assign($this->twofaccountOne->id); + + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->twofaccountOne->id, + 'group_id' => null, + ]); + } + + + /** + * @test + */ + public function test_getAccounts_returns_accounts() + { + $this->groupService->assign([$this->twofaccountOne->id, $this->twofaccountTwo->id], $this->groupOne); + $accounts = $this->groupService->getAccounts($this->groupOne); + + $this->assertEquals(2, $accounts->count()); + } + +} \ No newline at end of file diff --git a/tests/Feature/Services/QrCodeServiceTest.php b/tests/Feature/Services/QrCodeServiceTest.php new file mode 100644 index 00000000..fce0ddc7 --- /dev/null +++ b/tests/Feature/Services/QrCodeServiceTest.php @@ -0,0 +1,67 @@ +qrcodeService = $this->app->make('App\Services\QrCodeService'); + } + + + /** + * @test + */ + public function test_encode_returns_correct_value() + { + // $rendered = $this->qrcodeService->encode(self::STRING_TO_ENCODE); + $this->assertEquals(self::STRING_ENCODED, $this->qrcodeService->encode(self::STRING_TO_ENCODE)); + } + + + /** + * @test + */ + public function test_decode_valid_image_returns_correct_value() + { + $file = LocalFile::fake()->validQrcode(); + + $this->assertEquals(self::DECODED_IMAGE, $this->qrcodeService->decode($file)); + } + + + /** + * @test + */ + public function test_decode_invalid_image_returns_correct_value() + { + $this->expectException(\App\Exceptions\InvalidQrCodeException::class); + + $this->qrcodeService->decode(LocalFile::fake()->invalidQrcode()); + } + +} \ No newline at end of file diff --git a/tests/Feature/Services/AppstractOptionsServiceTest.php b/tests/Feature/Services/SettingServiceTest.php similarity index 50% rename from tests/Feature/Services/AppstractOptionsServiceTest.php rename to tests/Feature/Services/SettingServiceTest.php index 84821322..ed2ab2ba 100644 --- a/tests/Feature/Services/AppstractOptionsServiceTest.php +++ b/tests/Feature/Services/SettingServiceTest.php @@ -1,17 +1,29 @@ settingService = $this->app->make('App\Services\SettingServiceInterface'); + + $this->twofaccountOne = new TwoFAccount; + $this->twofaccountOne->legacy_uri = self::TOTP_FULL_CUSTOM_URI; + $this->twofaccountOne->service = self::SERVICE; + $this->twofaccountOne->account = self::ACCOUNT; + $this->twofaccountOne->icon = self::ICON; + $this->twofaccountOne->otp_type = 'totp'; + $this->twofaccountOne->secret = self::SECRET; + $this->twofaccountOne->digits = self::DIGITS_CUSTOM; + $this->twofaccountOne->algorithm = self::ALGORITHM_CUSTOM; + $this->twofaccountOne->period = self::PERIOD_CUSTOM; + $this->twofaccountOne->counter = null; + $this->twofaccountOne->save(); + + $this->twofaccountTwo = new TwoFAccount; + $this->twofaccountTwo->legacy_uri = self::TOTP_FULL_CUSTOM_URI; + $this->twofaccountTwo->service = self::SERVICE; + $this->twofaccountTwo->account = self::ACCOUNT; + $this->twofaccountTwo->icon = self::ICON; + $this->twofaccountTwo->otp_type = 'totp'; + $this->twofaccountTwo->secret = self::SECRET; + $this->twofaccountTwo->digits = self::DIGITS_CUSTOM; + $this->twofaccountTwo->algorithm = self::ALGORITHM_CUSTOM; + $this->twofaccountTwo->period = self::PERIOD_CUSTOM; + $this->twofaccountTwo->counter = null; + $this->twofaccountTwo->save(); } @@ -126,6 +173,98 @@ public function test_set_setting_persist_correct_value() self::VALUE => self::SETTING_VALUE_STRING ]); } + + + /** + * @test + */ + public function test_set_useEncryption_on_encrypts_all_accounts() + { + $this->settingService->set('useEncryption', true); + + $twofaccounts = DB::table('twofaccounts')->get(); + + $twofaccounts->each(function ($item, $key) { + $this->assertEquals(self::ACCOUNT, Crypt::decryptString($item->account)); + $this->assertEquals(self::SECRET, Crypt::decryptString($item->secret)); + $this->assertEquals(self::TOTP_FULL_CUSTOM_URI, Crypt::decryptString($item->legacy_uri)); + }); + } + + + /** + * @test + */ + public function test_set_useEncryption_on_twice_prevents_successive_encryption() + { + $this->settingService->set('useEncryption', true); + $this->settingService->set('useEncryption', true); + + $twofaccounts = DB::table('twofaccounts')->get(); + + $twofaccounts->each(function ($item, $key) { + $this->assertEquals(self::ACCOUNT, Crypt::decryptString($item->account)); + $this->assertEquals(self::SECRET, Crypt::decryptString($item->secret)); + $this->assertEquals(self::TOTP_FULL_CUSTOM_URI, Crypt::decryptString($item->legacy_uri)); + }); + } + + + /** + * @test + */ + public function test_set_useEncryption_off_decrypts_all_accounts() + { + $this->settingService->set('useEncryption', true); + $this->settingService->set('useEncryption', false); + + $twofaccounts = DB::table('twofaccounts')->get(); + + $twofaccounts->each(function ($item, $key) { + $this->assertEquals(self::ACCOUNT, $item->account); + $this->assertEquals(self::SECRET, $item->secret); + $this->assertEquals(self::TOTP_FULL_CUSTOM_URI, $item->legacy_uri); + }); + } + + + /** + * @test + * @dataProvider provideUndecipherableData + */ + public function test_set_useEncryption_off_returns_exception_when_data_are_undecipherable(array $data) + { + $this->expectException(\App\Exceptions\DbEncryptionException::class); + + $this->settingService->set('useEncryption', true); + + $affected = DB::table('twofaccounts') + ->where('id', $this->twofaccountOne->id) + ->update($data); + + $this->settingService->set('useEncryption', false); + + $twofaccount = TwoFAccount::find($this->twofaccountOne->id); + } + + + /** + * Provide invalid data for validation test + */ + public function provideUndecipherableData() : array + { + return [ + [[ + 'account' => 'undecipherableString' + ]], + [[ + 'secret' => 'undecipherableString' + ]], + [[ + 'legacy_uri' => 'undecipherableString' + ]], + ]; + } /** diff --git a/tests/Feature/Services/TwoFAccountServiceTest.php b/tests/Feature/Services/TwoFAccountServiceTest.php index 881a5653..c894c026 100644 --- a/tests/Feature/Services/TwoFAccountServiceTest.php +++ b/tests/Feature/Services/TwoFAccountServiceTest.php @@ -1,7 +1,8 @@ customHotpTwofaccount->period = null; $this->customHotpTwofaccount->counter = self::COUNTER_CUSTOM; $this->customHotpTwofaccount->save(); + + + $this->group = new Group; + $this->group->name = 'MyGroup'; + $this->group->save(); } @@ -532,6 +544,20 @@ public function test_getOTP_for_totp_with_invalid_secret_returns_InvalidSecretEx } + /** + * @test + */ + public function test_getOTP_for_totp_with_undecipherable_secret_returns_UndecipherableException() + { + $this->expectException(\App\Exceptions\UndecipherableException::class); + $otp_from_uri = $this->twofaccountService->getOTP([ + 'account' => self::ACCOUNT, + 'otp_type' => 'totp', + 'secret' => __('errors.indecipherable'), + ]); + } + + /** * @test */ @@ -618,4 +644,119 @@ public function test_getURI_for_totp_dto_returns_uri() $this->assertStringContainsString('secret='.self::SECRET, $uri); } + + /** + * @test + */ + public function test_withdraw_comma_separated_ids_deletes_relation() + { + $twofaccounts = collect([$this->customHotpTwofaccount, $this->customTotpTwofaccount]); + $this->group->twofaccounts()->saveMany($twofaccounts); + + $this->twofaccountService->withdraw($this->customHotpTwofaccount->id.','.$this->customTotpTwofaccount->id); + + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->customTotpTwofaccount->id, + 'group_id' => null, + ]); + + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->customHotpTwofaccount->id, + 'group_id' => null, + ]); + } + + + /** + * @test + */ + public function test_withdraw_array_of_model_ids_deletes_relation() + { + $twofaccounts = collect([$this->customHotpTwofaccount, $this->customTotpTwofaccount]); + $this->group->twofaccounts()->saveMany($twofaccounts); + + $this->twofaccountService->withdraw([$this->customHotpTwofaccount->id, $this->customTotpTwofaccount->id]); + + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->customTotpTwofaccount->id, + 'group_id' => null, + ]); + + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->customHotpTwofaccount->id, + 'group_id' => null, + ]); + } + + + /** + * @test + */ + public function test_withdraw_single_id_deletes_relation() + { + $twofaccounts = collect([$this->customHotpTwofaccount, $this->customTotpTwofaccount]); + $this->group->twofaccounts()->saveMany($twofaccounts); + + $this->twofaccountService->withdraw($this->customTotpTwofaccount->id); + + $this->assertDatabaseHas('twofaccounts', [ + 'id' => $this->customTotpTwofaccount->id, + 'group_id' => null, + ]); + } + + + /** + * @test + */ + public function test_withdraw_missing_ids_returns_void() + { + $this->assertNull($this->twofaccountService->withdraw(null)); + } + + + /** + * @test + */ + public function test_delete_comma_separated_ids() + { + $this->twofaccountService->delete($this->customHotpTwofaccount->id.','.$this->customTotpTwofaccount->id); + + $this->assertDatabaseMissing('twofaccounts', [ + 'id' => $this->customTotpTwofaccount->id, + ]); + $this->assertDatabaseMissing('twofaccounts', [ + 'id' => $this->customHotpTwofaccount->id, + ]); + } + + + /** + * @test + */ + public function test_delete_array_of_ids() + { + $this->twofaccountService->delete([$this->customTotpTwofaccount->id, $this->customHotpTwofaccount->id]); + + $this->assertDatabaseMissing('twofaccounts', [ + 'id' => $this->customTotpTwofaccount->id, + ]); + $this->assertDatabaseMissing('twofaccounts', [ + 'id' => $this->customHotpTwofaccount->id, + ]); + } + + + /** + * @test + */ + public function test_delete_single_id() + { + $this->twofaccountService->delete($this->customTotpTwofaccount->id); + + $this->assertDatabaseMissing('twofaccounts', [ + 'id' => $this->customTotpTwofaccount->id, + ]); + } + } \ No newline at end of file diff --git a/tests/ModelTestCase.php b/tests/ModelTestCase.php new file mode 100644 index 00000000..66d228df --- /dev/null +++ b/tests/ModelTestCase.php @@ -0,0 +1,138 @@ + `getFillable()` + * - `$guarded` -> `getGuarded()` + * - `$table` -> `getTable()` + * - `$primaryKey` -> `getKeyName()` + * - `$hidden` -> `getHidden()` + * - `$visible` -> `getVisible()` + * - `$casts` -> `getCasts()`: note that method appends incrementing key. + * - `$dates` -> `getDates()`: note that method appends `[static::CREATED_AT, static::UPDATED_AT]`. + * - `newCollection()`: assert collection is exact type. Use `assertEquals` on `get_class()` result, but not `assertInstanceOf`. + */ + protected function runConfigurationAssertions( + Model $model, + $fillable = [], + $hidden = [], + $guarded = ['*'], + $visible = [], + $casts = ['id' => 'int'], + $dispatchesEvents = [], + $dates = ['created_at', 'updated_at'], + $collectionClass = Collection::class, + $table = null, + $primaryKey = 'id', + $incrementing = true) + { + $this->assertEquals($fillable, $model->getFillable()); + $this->assertEquals($guarded, $model->getGuarded()); + $this->assertEquals($hidden, $model->getHidden()); + $this->assertEquals($visible, $model->getVisible()); + $this->assertEquals($casts, $model->getCasts()); + $this->assertEquals($dates, $model->getDates()); + $this->assertEquals($primaryKey, $model->getKeyName()); + $this->assertEquals($incrementing, $model->getIncrementing()); + + $eventDispatcher = $model->getEventDispatcher(); + foreach ($dispatchesEvents as $eventName => $eventclass) { + $this->assertTrue($eventDispatcher->hasListeners($eventclass)); + } + + $c = $model->newCollection(); + $this->assertEquals($collectionClass, get_class($c)); + $this->assertInstanceOf(Collection::class, $c); + + if ($table !== null) { + $this->assertEquals($table, $model->getTable()); + } + } + + + /** + * @param HasMany $relation + * @param Model $model + * @param Model $related + * @param string $key + * @param string $parent + * @param \Closure $queryCheck + * + * - `getQuery()`: assert query has not been modified or modified properly. + * - `getForeignKey()`: any `HasOneOrMany` or `BelongsTo` relation, but key type differs (see documentaiton). + * - `getQualifiedParentKeyName()`: in case of `HasOneOrMany` relation, there is no `getLocalKey()` method, so this one should be asserted. + */ + protected function assertHasManyRelation($relation, Model $model, Model $related, $key = null, $parent = null, \Closure $queryCheck = null) + { + $this->assertInstanceOf(HasMany::class, $relation); + + if (!is_null($queryCheck)) { + $queryCheck->bindTo($this); + $queryCheck($relation->getQuery(), $model, $relation); + } + + if (is_null($key)) { + $key = $model->getForeignKey(); + } + + $this->assertEquals($key, $relation->getForeignKeyName()); + + if (is_null($parent)) { + $parent = $model->getKeyName(); + } + + $this->assertEquals($model->getTable().'.'.$parent, $relation->getQualifiedParentKeyName()); + } + + + /** + * @param BelongsTo $relation + * @param Model $model + * @param Model $related + * @param string $key + * @param string $owner + * @param \Closure $queryCheck + * + * - `getQuery()`: assert query has not been modified or modified properly. + * - `getForeignKey()`: any `HasOneOrMany` or `BelongsTo` relation, but key type differs (see documentaiton). + * - `getOwnerKey()`: `BelongsTo` relation and its extendings. + */ + protected function assertBelongsToRelation($relation, Model $model, Model $related, $key, $owner = null, \Closure $queryCheck = null) + { + $this->assertInstanceOf(BelongsTo::class, $relation); + + if (!is_null($queryCheck)) { + $queryCheck->bindTo($this); + $queryCheck($relation->getQuery(), $model, $relation); + } + + $this->assertEquals($key, $relation->getForeignKey()); + + if (is_null($owner)) { + $owner = $related->getKeyName(); + } + + $this->assertEquals($owner, $relation->getOwnerKey()); + } +} diff --git a/tests/Unit/Api/v1/Controllers/GroupControllerTest.php b/tests/Unit/Api/v1/Controllers/GroupControllerTest.php new file mode 100644 index 00000000..6e79ea6e --- /dev/null +++ b/tests/Unit/Api/v1/Controllers/GroupControllerTest.php @@ -0,0 +1,186 @@ +groupServiceMock = Mockery::mock($this->app->make(GroupService::class)); + $this->groupStoreRequest = Mockery::mock('App\Api\v1\Requests\GroupStoreRequest'); + + $this->controller = new GroupController($this->groupServiceMock); + } + + + /** + * @test + */ + public function test_index_returns_api_resources_using_groupService() + { + $groups = factory(Group::class, 3)->make(); + + $this->groupServiceMock->shouldReceive('getAll') + ->once() + ->andReturn($groups); + + $response = $this->controller->index(); + + $this->assertContainsOnlyInstancesOf('App\Api\v1\Resources\GroupResource', $response->collection); + } + + + /** + * @test + */ + public function test_store_returns_api_resource_stored_using_groupService() + { + $group = factory(Group::class)->make(); + + $this->groupStoreRequest->shouldReceive('validated') + ->once() + ->andReturn(['name' => $group->name]); + + $this->groupServiceMock->shouldReceive('create') + ->once() + ->andReturn($group); + + $response = $this->controller->store($this->groupStoreRequest); + + $this->assertInstanceOf('App\Group', $response->original); + } + + + /** + * @test + */ + public function test_show_returns_api_resource() + { + $group = factory(Group::class)->make(); + + $response = $this->controller->show($group); + + $this->assertInstanceOf('App\Api\v1\Resources\GroupResource', $response); + } + + + /** + * @test + */ + public function test_update_returns_api_resource_updated_using_groupService() + { + $group = factory(Group::class)->make(); + + $this->groupStoreRequest->shouldReceive('validated') + ->once() + ->andReturn(['name' => $group->name]); + + $this->groupServiceMock->shouldReceive('update') + ->once() + ->andReturn($group); + + $response = $this->controller->update($this->groupStoreRequest, $group); + + $this->assertInstanceOf('App\Api\v1\Resources\GroupResource', $response); + } + + + /** + * @test + */ + public function test_assignAccounts_returns_api_resource_assigned_using_groupService() + { + $group = factory(Group::class)->make(); + $groupAssignRequest = Mockery::mock('App\Api\v1\Requests\GroupAssignRequest'); + + $groupAssignRequest->shouldReceive('validated') + ->once() + ->andReturn(['ids' => $group->id]); + + $this->groupServiceMock->shouldReceive('assign') + ->with($group->id, $group) + ->once(); + + $response = $this->controller->assignAccounts($groupAssignRequest, $group); + + $this->assertInstanceOf('App\Api\v1\Resources\GroupResource', $response); + } + + + /** + * @test + */ + public function test_accounts_returns_api_resources_fetched_using_groupService() + { + $group = factory(Group::class)->make(); + + \Facades\App\Services\SettingServiceInterface::shouldReceive('get') + ->with('useEncryption') + ->andReturn(false); + + $twofaccounts = factory(TwoFAccount::class, 3)->make(); + + $this->groupServiceMock->shouldReceive('getAccounts') + ->with($group) + ->once() + ->andReturn($twofaccounts); + + $response = $this->controller->accounts($group); + // TwoFAccountCollection + $this->assertContainsOnlyInstancesOf('App\Api\v1\Resources\TwoFAccountReadResource', $response->collection); + } + + + /** + * @test + */ + public function test_destroy_uses_group_service() + { + $group = factory(Group::class)->make(); + + $this->groupServiceMock->shouldReceive('delete') + ->once() + ->with($group->id); + + $response = $this->controller->destroy($group); + + $this->assertInstanceOf('Illuminate\Http\JsonResponse', $response); + } +} \ No newline at end of file diff --git a/tests/Unit/Events/GroupDeletingTest.php b/tests/Unit/Events/GroupDeletingTest.php new file mode 100644 index 00000000..1953b84f --- /dev/null +++ b/tests/Unit/Events/GroupDeletingTest.php @@ -0,0 +1,25 @@ +make(); + $event = new GroupDeleting($group); + + $this->assertSame($group, $event->group); + } +} \ No newline at end of file diff --git a/tests/Unit/Events/TwoFAccountDeletedTest.php b/tests/Unit/Events/TwoFAccountDeletedTest.php new file mode 100644 index 00000000..a15007d8 --- /dev/null +++ b/tests/Unit/Events/TwoFAccountDeletedTest.php @@ -0,0 +1,29 @@ +with('useEncryption') + ->andReturn(false); + + $twofaccount = factory(TwoFAccount::class)->make(); + $event = new TwoFAccountDeleted($twofaccount); + + $this->assertSame($twofaccount, $event->twofaccount); + } +} \ No newline at end of file diff --git a/tests/Unit/Exceptions/HandlerTest.php b/tests/Unit/Exceptions/HandlerTest.php new file mode 100644 index 00000000..dfb7f6b9 --- /dev/null +++ b/tests/Unit/Exceptions/HandlerTest.php @@ -0,0 +1,103 @@ +createMock(Request::class); + $instance = new Handler($this->createMock(Container::class)); + $class = new \ReflectionClass(Handler::class); + + $method = $class->getMethod('render'); + $method->setAccessible(true); + + $response = $method->invokeArgs($instance, [$request, $this->createMock($exception)]); + + $this->assertInstanceOf(JsonResponse::class, $response); + + $response = \Illuminate\Testing\TestResponse::fromBaseResponse($response); + $response->assertStatus(400) + ->assertJsonStructure([ + 'message' + ]); + } + + /** + * Provide Valid data for validation test + */ + public function provideExceptionsforBadRequest() : array + { + return [ + [ + '\App\Exceptions\InvalidOtpParameterException' + ], + [ + '\App\Exceptions\InvalidQrCodeException' + ], + [ + '\App\Exceptions\InvalidSecretException' + ], + [ + '\App\Exceptions\DbEncryptionException' + ], + ]; + } + + /** + * @test + * + * @dataProvider provideExceptionsforNotFound + */ + public function test_exceptions_returns_notFound_json_response($exception) + { + $request = $this->createMock(Request::class); + $instance = new Handler($this->createMock(Container::class)); + $class = new \ReflectionClass(Handler::class); + + $method = $class->getMethod('render'); + $method->setAccessible(true); + + $response = $method->invokeArgs($instance, [$request, $this->createMock($exception)]); + + $this->assertInstanceOf(JsonResponse::class, $response); + + $response = \Illuminate\Testing\TestResponse::fromBaseResponse($response); + $response->assertStatus(404) + ->assertJsonStructure([ + 'message' + ]); + } + + /** + * Provide Valid data for validation test + */ + public function provideExceptionsforNotFound() : array + { + return [ + [ + '\Illuminate\Database\Eloquent\ModelNotFoundException' + ], + [ + '\Symfony\Component\HttpKernel\Exception\NotFoundHttpException' + ], + ]; + } +} \ No newline at end of file diff --git a/tests/Unit/GroupModelTest.php b/tests/Unit/GroupModelTest.php new file mode 100644 index 00000000..941e7ed0 --- /dev/null +++ b/tests/Unit/GroupModelTest.php @@ -0,0 +1,43 @@ +runConfigurationAssertions( + new Group(), + ['name'], + ['created_at', 'updated_at'], + ['*'], + [], + ['id' => 'int', 'twofaccounts_count' => 'integer',], + ['deleting' => GroupDeleting::class] + ); + } + + + /** + * @test + */ + public function test_groups_relation() + { + $group = new Group(); + $accounts = $group->twofaccounts(); + $this->assertHasManyRelation($accounts, $group, new TwoFAccount()); + } +} \ No newline at end of file diff --git a/tests/Unit/Listeners/CleanIconStorageTest.php b/tests/Unit/Listeners/CleanIconStorageTest.php new file mode 100644 index 00000000..e372938f --- /dev/null +++ b/tests/Unit/Listeners/CleanIconStorageTest.php @@ -0,0 +1,33 @@ +with('useEncryption') + ->andReturn(false); + + $twofaccount = factory(TwoFAccount::class)->make(); + $event = new TwoFAccountDeleted($twofaccount); + $listener = new CleanIconStorage(); + + Storage::shouldReceive('delete') + ->with('public/icons/' . $event->twofaccount->icon) + ->andReturn(true); + + $this->assertNull($listener->handle($event)); + } +} \ No newline at end of file diff --git a/tests/Unit/Listeners/DissociateTwofaccountFromGroupTest.php b/tests/Unit/Listeners/DissociateTwofaccountFromGroupTest.php new file mode 100644 index 00000000..03d3758c --- /dev/null +++ b/tests/Unit/Listeners/DissociateTwofaccountFromGroupTest.php @@ -0,0 +1,26 @@ +make(); + $event = new GroupDeleting($group); + $listener = new DissociateTwofaccountFromGroup(); + + $this->assertNull($listener->handle($event)); + } +} \ No newline at end of file diff --git a/tests/Unit/TwoFAccountModelTest.php b/tests/Unit/TwoFAccountModelTest.php new file mode 100644 index 00000000..fecba269 --- /dev/null +++ b/tests/Unit/TwoFAccountModelTest.php @@ -0,0 +1,111 @@ +runConfigurationAssertions( + new TwoFAccount(), + [], + [], + ['*'], + [], + ['id' => 'int'], + ['deleted' => TwoFAccountDeleted::class], + ['created_at', 'updated_at'], + \Illuminate\Database\Eloquent\Collection::class, + 'twofaccounts', + 'id', + true + ); + } + + + /** + * @test + * + * @dataProvider provideSensitiveAttributes + */ + public function test_sensitive_attributes_are_stored_encrypted(string $attribute) + { + \Facades\App\Services\SettingServiceInterface::shouldReceive('get') + ->with('useEncryption') + ->andReturn(true); + + $twofaccount = factory(TwoFAccount::class)->make([ + $attribute => 'string', + ]); + + $this->assertEquals('string', Crypt::decryptString($twofaccount->getAttributes()[$attribute])); + } + + /** + * Provide attributes to test for encryption + */ + public function provideSensitiveAttributes() : array + { + return [ + [ + 'legacy_uri' + ], + [ + 'secret' + ], + [ + 'account' + ], + ]; + } + + /** + * @test + * + * @dataProvider provideSensitiveAttributes + */ + public function test_sensitive_attributes_are_returned_clear(string $attribute) + { + \Facades\App\Services\SettingServiceInterface::shouldReceive('get') + ->with('useEncryption') + ->andReturn(false); + + $twofaccount = factory(TwoFAccount::class)->make(); + + $this->assertEquals($twofaccount->getAttributes()[$attribute], $twofaccount->$attribute); + } + + + /** + * @test + * + * @dataProvider provideSensitiveAttributes + */ + public function test_indecipherable_attributes_returns_masked_value(string $attribute) + { + \Facades\App\Services\SettingServiceInterface::shouldReceive('get') + ->with('useEncryption') + ->andReturn(true); + + Crypt::shouldReceive('encryptString') + ->andReturn('indecipherableString'); + + $twofaccount = factory(TwoFAccount::class)->make(); + + $this->assertEquals(__('errors.indecipherable'), $twofaccount->$attribute); + } +} \ No newline at end of file diff --git a/tests/Unit/UserModelTest.php b/tests/Unit/UserModelTest.php new file mode 100644 index 00000000..a3efeb62 --- /dev/null +++ b/tests/Unit/UserModelTest.php @@ -0,0 +1,39 @@ +runConfigurationAssertions(new User(), + ['name', 'email', 'password'], + ['password', 'remember_token'], + ['*'], + [], + ['id' => 'int', 'email_verified_at' => 'datetime'] + ); + } + + /** + * @test + */ + public function test_email_is_set_lowercased() + { + $user = factory(User::class)->make([ + 'email' => 'UPPERCASE@example.COM', + ]); + + $this->assertEquals(strtolower('UPPERCASE@example.COM'), $user->email); + } +} \ No newline at end of file