Bläddra i källkod

Reorganize and enhance tests

Bubka 3 år sedan
förälder
incheckning
45b835bbd5

+ 11 - 2
app/Services/TwoFAccountService.php

@@ -208,7 +208,9 @@ class TwoFAccountService
             
             Log::info(sprintf('TwoFAccounts #%s withdrawn', implode(',#', $ids)));
         }
+        // @codeCoverageIgnoreStart
         else Log::info('No TwoFAccount to withdraw');
+        // @codeCoverageIgnoreEnd
     }
 
 
@@ -289,8 +291,13 @@ class TwoFAccountService
     {
         $dto = new TwoFAccountDto();
 
-        foreach ($array as $key => $value) {
-            $dto->$key = ! Arr::has($array, $key) ?: $value;
+        try {
+            foreach ($array as $key => $value) {
+                $dto->$key = ! Arr::has($array, $key) ?: $value;
+            }
+        }
+        catch (\TypeError $ex) {
+            throw new InvalidOtpParameterException($ex->getMessage());
         }
 
         return $dto;
@@ -457,8 +464,10 @@ class TwoFAccountService
                 Log::info(sprintf('Icon file %s stored', $newFilename));
             }
             else {
+                // @codeCoverageIgnoreStart
                 Storage::delete($imageFile);
                 throw new \Exception;
+                // @codeCoverageIgnoreEnd
             }
                 
             return $newFilename;

+ 4 - 0
composer.json

@@ -69,6 +69,10 @@
         ],
         "test" : [
             "vendor/bin/phpunit"
+        ],
+        "test-coverage-html" : [
+            "@putenv XDEBUG_MODE=coverage",
+            "vendor/bin/phpunit --coverage-html tests/Coverage/"
         ]
     }
 }

+ 3 - 0
phpunit.xml

@@ -23,6 +23,9 @@
         <testsuite name="Feature">
             <directory suffix="Test.php">./tests/Feature</directory>
         </testsuite>
+        <testsuite name="Api.v1">
+            <directory suffix="Test.php">./tests/Api/v1</directory>
+        </testsuite>
     </testsuites>
     <php>
         <server name="APP_ENV" value="testing"/>

+ 95 - 0
tests/Api/v1/Controllers/Auth/ForgotPasswordControllerTest.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace Tests\Feature\Auth;
+
+use App\User;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Config;
+use Illuminate\Auth\Notifications\ResetPassword;
+use Illuminate\Support\Facades\Notification;
+use Tests\FeatureTestCase;
+
+class ForgotPasswordControllerTest extends FeatureTestCase
+{
+    /**
+     * @var \App\User
+     */
+    protected $user;
+
+    /**
+     * @test
+     */
+    public function test_submit_email_password_request_without_email_returns_validation_error()
+    {
+        $response = $this->json('POST', '/api/v1/user/password/lost', [
+            'email' => ''
+        ]);
+
+        $response->assertStatus(422)
+                 ->assertJsonValidationErrors(['email']);
+    }
+
+    /**
+     * @test
+     */
+    public function test_submit_email_password_request_with_invalid_email_returns_validation_error()
+    {
+        $response = $this->json('POST', '/api/v1/user/password/lost', [
+            'email' => 'nametest.com'
+        ]);
+
+        $response->assertStatus(422)
+                 ->assertJsonValidationErrors(['email']);
+    }
+
+    /**
+     * @test
+     */
+    public function test_submit_email_password_request_with_unknown_email_returns_validation_error()
+    {
+        $response = $this->json('POST', '/api/v1/user/password/lost', [
+            'email' => 'name@test.com'
+        ]);
+
+        $response->assertStatus(422)
+                 ->assertJsonValidationErrors(['email']);
+    }
+
+    /**
+     * @test
+     */
+    public function test_submit_email_password_request_returns_success()
+    {
+        Notification::fake();
+
+        $this->user = factory(User::class)->create();
+
+        $response = $this->json('POST', '/api/v1/user/password/lost', [
+            'email' => $this->user->email
+        ]);
+
+        $response->assertStatus(200);
+
+        $token = \Illuminate\Support\Facades\DB::table('password_resets')->first();
+        $this->assertNotNull($token);
+
+        Notification::assertSentTo($this->user, ResetPassword::class, function ($notification, $channels) use ($token) {
+            return Hash::check($notification->token, $token->token) === true;
+        });
+    }
+
+    /**
+     * @test
+     */
+    public function test_submit_email_password_request__in_demo_mode_returns_unauthorized()
+    {
+        Config::set('2fauth.config.isDemoApp', true);
+
+        $response = $this->json('POST', '/api/v1/user/password/lost', [
+            'email' => ''
+        ]);
+
+        $response->assertStatus(401);
+    }
+
+}

+ 81 - 0
tests/Api/v1/Controllers/Auth/PasswordControllerTest.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace Tests\Api\v1\Controllers\Auth;
+
+use App\User;
+use App\Group;
+use Tests\FeatureTestCase;
+use App\TwoFAccount;
+
+class PasswordControllerTest extends FeatureTestCase
+{
+    /**
+     * @var \App\User
+    */
+    protected $user;
+
+    private const PASSWORD =  'password';
+    private const NEW_PASSWORD =  'newPassword';
+
+    /**
+     * @test
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        $this->user = factory(User::class)->create();
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_update_return_success()
+    {
+        $response = $this->actingAs($this->user, 'api')
+            ->json('PATCH', '/api/v1/user/password', [
+                'currentPassword' => self::PASSWORD,
+                'password' => self::NEW_PASSWORD,
+                'password_confirmation' => self::NEW_PASSWORD,
+            ])
+            ->assertOk()
+            ->assertJsonStructure([
+                'message',
+            ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_update_passing_bad_current_pwd_return_bad_request()
+    {
+        $response = $this->actingAs($this->user, 'api')
+            ->json('PATCH', '/api/v1/user/password', [
+                'currentPassword' => self::NEW_PASSWORD,
+                'password' => self::NEW_PASSWORD,
+                'password_confirmation' => self::NEW_PASSWORD,
+            ])
+            ->assertStatus(400)
+            ->assertJsonStructure([
+                'message',
+            ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_update_passing_invalid_data_return_validation_error()
+    {
+        $response = $this->actingAs($this->user, 'api')
+            ->json('PATCH', '/api/v1/user/password', [
+                'currentPassword' => self::PASSWORD,
+                'password' => null,
+                'password_confirmation' => self::NEW_PASSWORD,
+            ])
+            ->assertStatus(422);
+    }
+
+}

+ 59 - 0
tests/Api/v1/Controllers/Auth/RegisterControllerTest.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace Tests\Api\v1\Controllers\Auth;
+
+use Tests\FeatureTestCase;
+
+class RegisterControllerTest extends FeatureTestCase
+{
+    private const USERNAME = 'john doe';
+    private const EMAIL = 'johndoe@example.org';
+    private const PASSWORD =  'password';
+
+
+    /**
+     * @test
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+    }
+    
+
+    /**
+     * @test
+     */
+    public function test_register_returns_success()
+    {
+        $response = $this->json('POST', '/api/v1/user', [
+                'name' => self::USERNAME,
+                'email' => self::EMAIL,
+                'password' => self::PASSWORD,
+                'password_confirmation' => self::PASSWORD,
+        ])
+        ->assertCreated()
+        ->assertJsonStructure([
+            'message',
+            'name',
+        ])
+        ->assertJsonFragment([
+            'name' => self::USERNAME,
+        ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_register_with_invalid_data_returns_validation_error()
+    {
+        $response = $this->json('POST', '/api/v1/user', [
+                'name' => null,
+                'email' => self::EMAIL,
+                'password' => self::PASSWORD,
+                'password_confirmation' => self::PASSWORD,
+            ])
+            ->assertStatus(422);
+    }
+
+}

+ 19 - 27
tests/Feature/Auth/ResetPasswordTest.php → tests/Api/v1/Controllers/Auth/ResetPasswordControllerTest.php

@@ -6,21 +6,22 @@ use App\User;
 use Illuminate\Support\Facades\Hash;
 use Illuminate\Support\Facades\Password;
 use Illuminate\Support\Facades\Notification;
-use Tests\TestCase;
+use Tests\FeatureTestCase;
 
-class ResetPasswordTest extends TestCase
+class ResetPasswordControllerTest extends FeatureTestCase
 {
-    /** @var \App\User */
+    /**
+     * @var \App\User
+     */
     protected $user;
 
 
     /**
-     * Testing submitting the reset password  without
-     * email address.
+     * @test
      */
-    public function testSubmitResetPasswordWithoutInput()
+    public function test_submit_reset_password_without_input_returns_validation_error()
     {
-        $response = $this->json('POST', '/api/password/reset', [
+        $response = $this->json('POST', '/api/v1/user/password/reset', [
             'email' => '',
             'password' => '',
             'password_confirmation' => '',
@@ -32,12 +33,11 @@ class ResetPasswordTest extends TestCase
     }
 
     /**
-     * Testing submitting the reset password with
-     * invalid input.
+     * @test
      */
-    public function testSubmitResetPasswordWithInvalidInput()
+    public function test_submit_reset_password_with_invalid_data_returns_validation_error()
     {
-        $response = $this->json('POST', '/api/password/reset', [
+        $response = $this->json('POST', '/api/v1/user/password/reset', [
             'email' => 'qsdqsdqsd',
             'password' => 'foofoofoo',
             'password_confirmation' => 'barbarbar',
@@ -49,12 +49,11 @@ class ResetPasswordTest extends TestCase
     }
 
     /**
-     * Testing submitting the reset password with
-     * invalid input.
+     * @test
      */
-    public function testSubmitResetPasswordWithTooShortPasswords()
+    public function test_submit_reset_password_with_too_short_pwd_returns_validation_error()
     {
-        $response = $this->json('POST', '/api/password/reset', [
+        $response = $this->json('POST', '/api/v1/user/password/reset', [
             'email' => 'foo@bar.com',
             'password' => 'foo',
             'password_confirmation' => 'foo',
@@ -66,23 +65,16 @@ class ResetPasswordTest extends TestCase
     }
 
     /**
-     * Testing submitting the rest password.
+     * @test
      */
-    public function testSubmitResetPassword()
+    public function test_submit_reset_password_returns_success()
     {
         Notification::fake();
 
-        $this->user = factory(User::class)->create([
-            'name' => 'user',
-            'email' => 'user@example.org',
-            'password' => bcrypt('password'),
-            'email_verified_at' => now(),
-            'remember_token' => \Illuminate\Support\Str::random(10)
-        ]);
-
+        $this->user = factory(User::class)->create();
         $token = Password::broker()->createToken($this->user);
 
-        $response = $this->json('POST', '/api/password/reset', [
+        $response = $this->json('POST', '/api/v1/user/password/reset', [
             'email' => $this->user->email,
             'password' => 'newpassword',
             'password_confirmation' => 'newpassword',
@@ -91,7 +83,7 @@ class ResetPasswordTest extends TestCase
 
         $this->user->refresh();
 
-        $response->assertStatus(200);
+        $response->assertOk();
         $this->assertTrue(Hash::check('newpassword', $this->user->password));
 
     }

+ 144 - 0
tests/Api/v1/Controllers/Auth/UserControllerTest.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace Tests\Api\v1\Controllers\Auth;
+
+use App\User;
+use Tests\FeatureTestCase;
+
+class UserControllerTest extends FeatureTestCase
+{
+    /**
+     * @var \App\User
+    */
+    protected $user;
+
+    private const NEW_USERNAME = 'Jane DOE';
+    private const NEW_EMAIL = 'janedoe@example.org';
+    private const PASSWORD =  'password';
+
+    /**
+     * @test
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        $this->user = factory(User::class)->create();
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_show_existing_user_when_authenticated_returns_success()
+    {
+        $response = $this->actingAs($this->user, 'api')
+            ->json('GET', '/api/v1/user')
+            ->assertOk()
+            ->assertExactJson([
+                'name'  => $this->user->name,
+                'email' => $this->user->email,
+            ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_show_existing_user_when_anonymous_returns_success()
+    {
+        $response = $this->json('GET', '/api/v1/user/name')
+            ->assertOk()
+            ->assertExactJson([
+                'name'  => $this->user->name,
+            ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_show_missing_user_returns_success_with_null_name()
+    {
+        User::destroy($this->user->id);
+
+        $response = $this->actingAs($this->user, 'api')
+            ->json('GET', '/api/v1/user')
+            ->assertOk()
+            ->assertExactJson([
+                'name'  => null,
+            ]);
+    }
+    
+
+    /**
+     * @test
+     */
+    public function test_update_user_returns_success()
+    {
+        $response = $this->actingAs($this->user, 'api')
+            ->json('PUT', '/api/v1/user', [
+                'name' => self::NEW_USERNAME,
+                'email' => self::NEW_EMAIL,
+                'password' => self::PASSWORD,
+            ])
+            ->assertOk()
+            ->assertExactJson([
+                'name' => self::NEW_USERNAME,
+                'email' => self::NEW_EMAIL,
+            ]);
+    }
+    
+
+    /**
+     * @test
+     */
+    public function test_update_user_in_demo_mode_returns_unchanged_user()
+    {
+        $settingService = resolve('App\Services\SettingServiceInterface');
+        $settingService->set('isDemoApp', true);
+
+        $response = $this->actingAs($this->user, 'api')
+            ->json('PUT', '/api/v1/user', [
+                'name' => self::NEW_USERNAME,
+                'email' => self::NEW_EMAIL,
+                'password' => self::PASSWORD,
+            ])
+            ->assertOk()
+            ->assertExactJson([
+                'name' => $this->user->name,
+                'email' => $this->user->email,
+            ]);
+    }
+    
+
+    /**
+     * @test
+     */
+    public function test_update_user_passing_wrong_password_returns_bad_request()
+    {
+        $response = $this->actingAs($this->user, 'api')
+            ->json('PUT', '/api/v1/user', [
+                'name' => self::NEW_USERNAME,
+                'email' => self::NEW_EMAIL,
+                'password' => 'wrongPassword',
+            ])
+            ->assertStatus(400);
+    }
+    
+
+    /**
+     * @test
+     */
+    public function test_update_user_with_invalid_data_returns_validation_error()
+    {
+        $response = $this->actingAs($this->user, 'api')
+            ->json('PUT', '/api/v1/user', [
+                'name' => '',
+                'email' => '',
+                'password' => self::PASSWORD,
+            ])
+            ->assertStatus(422);
+    }
+
+}

+ 4 - 0
tests/Api/v1/Controllers/GroupControllerTest.php

@@ -7,6 +7,10 @@ use App\Group;
 use Tests\FeatureTestCase;
 use App\TwoFAccount;
 
+
+/**
+ * @covers \App\Api\v1\Controllers\GroupController
+ */
 class GroupControllerTest extends FeatureTestCase
 {
     /**

+ 4 - 0
tests/Api/v1/Controllers/IconControllerTest.php

@@ -6,6 +6,10 @@ use Illuminate\Http\UploadedFile;
 use Illuminate\Foundation\Testing\WithoutMiddleware;
 use Tests\TestCase;
 
+
+/**
+ * @covers \App\Api\v1\Controllers\IconController
+ */
 class IconControllerTest extends TestCase
 {
 

+ 5 - 1
tests/Api/v1/Controllers/QrcodeControllerTest.php → tests/Api/v1/Controllers/QrCodeControllerTest.php

@@ -7,7 +7,11 @@ use Tests\FeatureTestCase;
 use App\TwoFAccount;
 use Tests\Classes\LocalFile;
 
-class QrcodeControllerTest extends FeatureTestCase
+
+/**
+ * @covers \App\Api\v1\Controllers\QrCodeController
+ */
+class QrCodeControllerTest extends FeatureTestCase
 {
 
     /**

+ 4 - 0
tests/Api/v1/Controllers/SettingControllerTest.php

@@ -7,6 +7,10 @@ use App\Group;
 use Tests\FeatureTestCase;
 use App\TwoFAccount;
 
+
+/**
+ * @covers \App\Api\v1\Controllers\SettingController
+ */
 class SettingControllerTest extends FeatureTestCase
 {
     /**

+ 92 - 0
tests/Api/v1/Controllers/TwoFAccountControllerTest.php

@@ -3,11 +3,16 @@
 namespace Tests\Api\v1\Unit;
 
 use App\User;
+use App\Group;
 use Tests\FeatureTestCase;
 use App\TwoFAccount;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Storage;
 
+
+/**
+ * @covers \App\Api\v1\Controllers\TwoFAccountController
+ */
 class TwoFAccountControllerTest extends FeatureTestCase
 {
     /**
@@ -15,6 +20,11 @@ class TwoFAccountControllerTest extends FeatureTestCase
     */
     protected $user;
 
+    /**
+     * @var \App\Group
+    */
+    protected $group;
+
     private const ACCOUNT = 'account';
     private const SERVICE = 'service';
     private const SECRET = 'A4GRFHVVRBGY7UIW';
@@ -156,6 +166,7 @@ class TwoFAccountControllerTest extends FeatureTestCase
         parent::setUp();
 
         $this->user = factory(User::class)->create();
+        $this->group = factory(Group::class)->create();
     }
 
 
@@ -419,6 +430,87 @@ class TwoFAccountControllerTest extends FeatureTestCase
     }
 
 
+    /**
+     * @test
+     */
+    public function test_store_assigns_created_account_when_default_group_is_a_specific_one()
+    {
+        // Set the default group to a specific one
+        $settingService = resolve('App\Services\SettingServiceInterface');
+        $settingService->set('defaultGroup', $this->group->id);
+
+        $response = $this->actingAs($this->user, 'api')
+            ->json('POST', '/api/v1/twofaccounts', [
+                'uri' => self::TOTP_SHORT_URI,
+            ])
+            ->assertJsonFragment([
+                'group_id' => $this->group->id
+            ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_store_assigns_created_account_when_default_group_is_the_active_one()
+    {
+        $settingService = resolve('App\Services\SettingServiceInterface');
+
+        // Set the default group to be the active one
+        $settingService->set('defaultGroup', -1);
+        // Set the active group
+        $settingService->set('activeGroup', 1);
+
+        $response = $this->actingAs($this->user, 'api')
+            ->json('POST', '/api/v1/twofaccounts', [
+                'uri' => self::TOTP_SHORT_URI,
+            ])
+            ->assertJsonFragment([
+                'group_id' => 1
+            ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_store_assigns_created_account_when_default_group_is_no_group()
+    {
+        $settingService = resolve('App\Services\SettingServiceInterface');
+
+        // Set the default group to No group
+        $settingService->set('defaultGroup', 0);
+
+        $response = $this->actingAs($this->user, 'api')
+            ->json('POST', '/api/v1/twofaccounts', [
+                'uri' => self::TOTP_SHORT_URI,
+            ])
+            ->assertJsonFragment([
+                'group_id' => null
+            ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_store_assigns_created_account_when_default_group_does_not_exist()
+    {
+        $settingService = resolve('App\Services\SettingServiceInterface');
+
+        // Set the default group to a non-existing one
+        $settingService->set('defaultGroup', 1000);
+
+        $response = $this->actingAs($this->user, 'api')
+            ->json('POST', '/api/v1/twofaccounts', [
+                'uri' => self::TOTP_SHORT_URI,
+            ])
+            ->assertJsonFragment([
+                'group_id' => null
+            ]);
+    }
+
+
     /**
      * @test
      */

+ 0 - 261
tests/Feature/AccountsGroupTest.php

@@ -1,261 +0,0 @@
-<?php
-
-namespace Tests\Feature;
-
-use App\User;
-use App\Group;
-use Tests\TestCase;
-use App\TwoFAccount;
-
-class AccountsGroupTest extends TestCase
-{
-    /** @var \App\User, \App\TwoFAccount, \App\Group */
-    protected $user, $twofaccounts, $group;
-
-
-    /**
-     * @test
-     */
-    public function setUp(): void
-    {
-        parent::setUp();
-
-        $this->user = factory(User::class)->create();
-        $this->twofaccounts = factory(Twofaccount::class, 3)->create();
-        $this->group = factory(Group::class)->create();
-    }
-
-
-    /**
-     * test 2FAccounts creation associated to a user group via API
-     *
-     * @test
-     */
-    public function testCreateAccountWhenDefaultGroupIsASpecificOne()
-    {
-
-        // Set the default group to the existing one
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'defaultGroup' => $this->group->id,
-                ])
-            ->assertStatus(200);
-
-        // Create the account
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/twofaccounts', [
-                    'service' => 'testCreation',
-                    'account' => 'test@example.org',
-                    'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHZVRBGY7UIW&issuer=test',
-                    'icon' => 'test.png',
-                ])
-            ->assertStatus(201)
-            ->assertJsonFragment([
-                'group_id' => $this->group->id
-            ]);
-    }
-
-
-    /**
-     * test 2FAccounts creation associated to a user group via API
-     *
-     * @test
-     */
-    public function testCreateAccountWhenDefaultGroupIsSetToActiveOne()
-    {
-
-        // Set the default group as the active one
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'defaultGroup' => -1,
-                ])
-            ->assertStatus(200);
-
-        // Set the active group
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'activeGroup' => 1,
-                ])
-            ->assertStatus(200);
-
-        // Create the account
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/twofaccounts', [
-                    'service' => 'testCreation',
-                    'account' => 'test@example.org',
-                    'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHZVRBGY7UIW&issuer=test',
-                    'icon' => 'test.png',
-                ])
-            ->assertStatus(201)
-            ->assertJsonFragment([
-                'group_id' => 1
-            ]);
-    }
-
-
-    /**
-     * test 2FAccounts creation associated to a user group via API
-     *
-     * @test
-     */
-    public function testCreateAccountWhenDefaultIsNoGroup()
-    {
-
-        // Set the default group to No group
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'defaultGroup' => 0,
-                ])
-            ->assertStatus(200);
-
-        // Create the account
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/twofaccounts', [
-                    'service' => 'testCreation',
-                    'account' => 'test@example.org',
-                    'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHZVRBGY7UIW&issuer=test',
-                    'icon' => 'test.png',
-                ])
-            ->assertStatus(201)
-            ->assertJsonMissing([
-                'group_id' => null
-            ]);
-    }
-
-
-    /**
-     * test 2FAccounts creation associated to a user group via API
-     *
-     * @test
-     */
-    public function testCreateAccountWhenDefaultGroupDoNotExists()
-    {
-
-        // Set the default group to a non existing one
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'defaultGroup' => 1000,
-                ])
-            ->assertStatus(200);
-
-        // Create the account
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/twofaccounts', [
-                    'service' => 'testCreation',
-                    'account' => 'test@example.org',
-                    'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHZVRBGY7UIW&issuer=test',
-                    'icon' => 'test.png',
-                ])
-            ->assertStatus(201)
-            ->assertJsonMissing([
-                'group_id' => null
-            ]);
-    }
-
-
-    /**
-     * test 2FAccounts association with a user group via API
-     *
-     * @test
-     */
-    public function testMoveAccountsToGroup()
-    {
-        // We associate all 3 accounts to the user group
-        $response = $this->actingAs($this->user, 'api')
-            ->json('PATCH', '/api/group/accounts/', [
-                    'groupId' => $this->group->id,
-                    'accountsIds' => [1,2,3]
-                ])
-            ->assertJsonFragment([
-                'id' => $this->group->id,
-                'name' => $this->group->name
-            ])
-            ->assertStatus(200);
-
-        // test if the accounts have the correct foreign key
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/1')
-            ->assertJsonFragment([
-                'group_id' => $this->group->id
-            ]);
-
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/2')
-            ->assertJsonFragment([
-                'group_id' => $this->group->id
-            ]);
-
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/3')
-            ->assertJsonFragment([
-                'group_id' => $this->group->id
-            ]);
-
-        // test the accounts count of the user group
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/groups')
-            ->assertJsonFragment([
-                'twofaccounts_count' => 3
-            ]
-        );
-    }
-
-
-    /**
-     * test 2FAccounts association with a missing group via API
-     *
-     * @test
-     */
-    public function testMoveAccountsToMissingGroup()
-    {
-        $response = $this->actingAs($this->user, 'api')
-            ->json('PATCH', '/api/group/accounts/', [
-                    'groupId' => '1000',
-                    'accountsIds' => $this->twofaccounts->keys()
-                ])
-            ->assertStatus(404);
-    }
-
-
-    /**
-     * test 2FAccounts association with the pseudo group via API
-     *
-     * @test
-     */
-    public function testMoveAccountsToPseudoGroup()
-    {
-
-        $response = $this->actingAs($this->user, 'api')
-            ->json('PATCH', '/api/group/accounts/', [
-                    'groupId' => $this->group->id,
-                    'accountsIds' => [1,2,3]
-                ]);
-
-        // We associate the first account to the pseudo group
-        $response = $this->actingAs($this->user, 'api')
-            ->json('PATCH', '/api/group/accounts/', [
-                    'groupId' => 0,
-                    'accountsIds' => [1]
-                ])
-            ->assertStatus(200);
-
-
-        // test if the forein keys are set to NULL
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/1')
-            ->assertJsonFragment([
-                'group_id' => null
-            ]);
-
-        // test the accounts count of the group
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/groups')
-            ->assertJsonFragment([
-                'twofaccounts_count' => 3, // the 3 accounts for 'all'
-                'twofaccounts_count' => 2  // the 2 accounts that remain in the user group
-            ]
-        );
-
-    }
-
-}

+ 0 - 103
tests/Feature/Auth/ForgotPasswordTest.php

@@ -1,103 +0,0 @@
-<?php
-
-namespace Tests\Feature\Auth;
-
-use App\User;
-use Illuminate\Support\Facades\Hash;
-use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Password;
-use Illuminate\Auth\Notifications\ResetPassword;
-use Illuminate\Support\Facades\Notification;
-use Tests\TestCase;
-
-class ForgotPasswordTest extends TestCase
-{
-    /** @var \App\User */
-    protected $user;
-
-    /**
-     * Testing submitting the email password request without
-     * email address.
-     */
-    public function testSubmitEmailPasswordRequestWithoutEmail()
-    {
-        $response = $this->json('POST', '/api/password/email', [
-            'email' => ''
-        ]);
-
-        $response->assertStatus(422)
-                 ->assertJsonValidationErrors(['email']);
-    }
-
-    /**
-     * Testing submitting the email password request with an invalid
-     * email address.
-     */
-    public function testSubmitEmailPasswordRequestWithInvalidEmail()
-    {
-        $response = $this->json('POST', '/api/password/email', [
-            'email' => 'nametest.com'
-        ]);
-
-        $response->assertStatus(422)
-                 ->assertJsonValidationErrors(['email']);
-    }
-
-    /**
-     * Testing submitting the email password request with an unknown
-     * email address.
-     */
-    public function testSubmitEmailPasswordRequestWithUnknownEmail()
-    {
-        $response = $this->json('POST', '/api/password/email', [
-            'email' => 'name@test.com'
-        ]);
-
-        $response->assertStatus(422)
-                 ->assertJsonValidationErrors(['email']);
-    }
-
-    /**
-     * Testing submitting the email password request with a valid email address.
-     */
-    public function testSubmitEmailPasswordRequest()
-    {
-        Notification::fake();
-
-        $this->user = factory(User::class)->create([
-            'name' => 'user',
-            'email' => 'user@example.org',
-            'password' => bcrypt('password'),
-            'email_verified_at' => now(),
-            'remember_token' => \Illuminate\Support\Str::random(10),
-        ]);
-
-        $response = $this->json('POST', '/api/password/email', [
-            'email' => $this->user->email
-        ]);
-
-        $response->assertStatus(200);
-
-        $token = \Illuminate\Support\Facades\DB::table('password_resets')->first();
-        $this->assertNotNull($token);
-
-        Notification::assertSentTo($this->user, ResetPassword::class, function ($notification, $channels) use ($token) {
-            return Hash::check($notification->token, $token->token) === true;
-        });
-    }
-
-    /**
-     * Testing submitting the email password request in Demo mode
-     */
-    public function testSubmitEmailPasswordRequestInDemoMode()
-    {
-        Config::set('2fauth.config.isDemoApp', true);
-
-        $response = $this->json('POST', '/api/password/email', [
-            'email' => ''
-        ]);
-
-        $response->assertStatus(401);
-    }
-
-}

+ 69 - 79
tests/Feature/Auth/LoginTest.php

@@ -3,18 +3,22 @@
 namespace Tests\Feature\Auth;
 
 use App\User;
-use Tests\TestCase;
+use Tests\FeatureTestCase;
 use Illuminate\Auth\Authenticatable;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Hash;
 use Illuminate\Auth\RequestGuard;
 use Illuminate\Support\Facades\Config;
 
-class LoginTest extends TestCase
+class LoginTest extends FeatureTestCase
 {
-    /** @var \App\User */
+    /**
+     * @var \App\User
+     */
     protected $user;
 
+    private const PASSWORD = 'password';
+    private const WRONG_PASSWORD = 'wrong_password';
 
     /**
      * @test
@@ -28,45 +32,38 @@ class LoginTest extends TestCase
 
 
     /**
-     * test User login via API
-     *
      * @test
      */
-    public function testUserLogin()
+    public function test_user_login_returns_success()
     {
-
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => $this->user->email,
-            'password' => 'password'
+            'password' => self::PASSWORD
+        ])
+        ->assertOk()
+        ->assertExactJson([
+            'message' => 'authenticated',
+            'name' => $this->user->name,
         ]);
-
-        $response->assertStatus(200)
-            ->assertJsonStructure([
-                'message' => ['token']
-            ]);
     }
 
 
     /**
-     * test User login via API
-     *
      * @test
      */
-    public function testUserLoginAlreadyAuthenticated()
+    public function test_user_login_already_authenticated_returns_bad_request()
     {
-
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => $this->user->email,
-            'password' => 'password'
+            'password' => self::PASSWORD
         ]);
 
         $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/login', [
+            ->json('POST', '/user/login', [
                 'email' => $this->user->email,
-                'password' => 'password'
-            ]);
-
-        $response->assertStatus(400)
+                'password' => self::PASSWORD
+            ])
+            ->assertStatus(400)
             ->assertJson([
                 'message' => __('auth.already_authenticated')
             ]);
@@ -74,79 +71,71 @@ class LoginTest extends TestCase
 
 
     /**
-     * test User login with missing values via API
-     *
      * @test
      */
-    public function testUserLoginWithMissingValues()
+    public function test_user_login_with_missing_data_returns_validation_error()
     {
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => '',
             'password' => ''
+        ])
+        ->assertStatus(422)
+        ->assertJsonValidationErrors([
+            'email',
+            'password'
         ]);
-
-        $response->assertStatus(422)
-            ->assertJsonValidationErrors([
-                'email',
-                'password'
-            ]);
     }
 
 
     /**
-     * test User login with invalid credentials via API
-     *
      * @test
      */
-    public function testUserLoginWithInvalidCredential()
+    public function test_user_login_with_invalid_credentials_returns_validation_error()
     {
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => $this->user->email,
-            'password' => 'badPassword'
+            'password' => self::WRONG_PASSWORD
+        ])
+        ->assertStatus(401)
+        ->assertJson([
+            'message' => 'unauthorised'
         ]);
-
-        $response->assertStatus(401)
-            ->assertJson([
-                'message' => 'unauthorised'
-            ]);
     }
 
 
     /**
-     * test User login with invalid credentials via API
-     *
      * @test
      */
-    public function testTooManyAttempsWithInvalidCredential()
+    public function test_too_many_login_attempts_with_invalid_credentials_returns_too_many_request_error()
     {
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => $this->user->email,
-            'password' => 'badPassword'
+            'password' => self::WRONG_PASSWORD
         ]);
 
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => $this->user->email,
-            'password' => 'badPassword'
+            'password' => self::WRONG_PASSWORD
         ]);
 
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => $this->user->email,
-            'password' => 'badPassword'
+            'password' => self::WRONG_PASSWORD
         ]);
 
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => $this->user->email,
-            'password' => 'badPassword'
+            'password' => self::WRONG_PASSWORD
         ]);
 
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => $this->user->email,
-            'password' => 'badPassword'
+            'password' => self::WRONG_PASSWORD
         ]);
 
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => $this->user->email,
-            'password' => 'badPassword'
+            'password' => self::WRONG_PASSWORD
         ]);
 
         $response->assertStatus(429);
@@ -154,46 +143,47 @@ class LoginTest extends TestCase
 
 
     /**
-     * test User logout via API
-     *
      * @test
      */
-    public function testUserLogout()
+    public function test_user_logout_returns_validation_success()
     {
-        $response = $this->json('POST', '/api/login', [
+        $response = $this->json('POST', '/user/login', [
             'email' => $this->user->email,
-            'password' => 'password'
+            'password' => self::PASSWORD
         ]);
 
-        $headers = ['Authorization' => "Bearer " . $response->original['message']['token']];
-
-        $response = $this->json('POST', '/api/logout', [], $headers)
-            ->assertStatus(200)
-            ->assertJson([
+        $response = $this->actingAs($this->user, 'api')
+            ->json('GET', '/user/logout')
+            ->assertOk()
+            ->assertExactJson([
                 'message' => 'signed out',
             ]);
     }
 
 
     /**
-     * test User logout after inactivity via API
-     *
      * @test
      */
-    public function testUserLogoutAfterInactivity()
+    public function test_user_logout_after_inactivity_returns_unauthorized()
     {
         // Set the autolock period to 1 minute
+        $settingService = resolve('App\Services\SettingServiceInterface');
+        $settingService->set('kickUserAfter', 1);
+
+        $response = $this->json('POST', '/user/login', [
+            'email' => $this->user->email,
+            'password' => self::PASSWORD
+        ]);
+
+        // Ping a protected endpoint to log last_seen_at time
         $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'kickUserAfter' => '1'])
-            ->assertStatus(200);
+            ->json('GET', '/api/v1/twofaccounts');
 
         sleep(61);
 
-        // Ping a restricted endpoint to log last_seen_at time
         $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/settings/account')
-            ->assertStatus(401);
+            ->json('GET', '/api/v1/twofaccounts')
+            ->assertUnauthorized();
     }
 
 }

+ 0 - 124
tests/Feature/Auth/RegisterTest.php

@@ -1,124 +0,0 @@
-<?php
-
-namespace Tests\Feature\Auth;
-
-use App\User;
-use Tests\TestCase;
-
-class RegisterTest extends TestCase
-{
-    /** @var \App\User */
-    protected $user;
-
-
-    /**
-     * @test
-     */
-    public function setUp(): void
-    {
-        parent::setUp();
-
-        $this->user = factory(User::class)->create();
-    }
-
-
-    /**
-     * test Existing user count via API
-     *
-     * @test
-     */
-    public function testExistingUserCount()
-    {
-        $response = $this->json('POST', '/api/checkuser')
-            ->assertStatus(200)
-            ->assertJson([
-                'username' => $this->user->name,
-            ]);
-    }
-
-
-    /**
-     * test creation of another user via API
-     *
-     * @test
-     */
-    public function testUserCreationWithAnExistingUser()
-    {
-        $response = $this->json('POST', '/api/register', [
-            'name' => 'testCreate',
-            'email' => 'testCreate@example.org',
-            'password' => 'test',
-            'password_confirmation' => 'test',
-        ]);
-
-        $response->assertStatus(422);
-    }
-
-
-    /**
-     * test User creation with missing values via API
-     *
-     * @test
-     */
-    public function testUserCreationWithMissingValues()
-    {
-        // we delete the existing user
-        User::destroy(1);
-
-        $response = $this->json('POST', '/api/register', [
-            'name' => '',
-            'email' => '',
-            'password' => '',
-            'password_confirmation' => '',
-        ]);
-
-        $response->assertStatus(422);
-    }
-
-
-    /**
-     * test User creation with invalid values via API
-     *
-     * @test
-     */
-    public function testUserCreationWithInvalidData()
-    {
-        // we delete the existing user
-        User::destroy(1);
-
-        $response = $this->json('POST', '/api/register', [
-            'name' => 'testInvalid',
-            'email' => 'email',
-            'password' => 'test',
-            'password_confirmation' => 'tset',
-        ]);
-
-        $response->assertStatus(422);
-    }
-
-
-    /**
-     * test User creation via API
-     *
-     * @test
-     */
-    public function testUserCreation()
-    {
-
-        // we delete the existing user
-        User::destroy(1);
-
-        $response = $this->json('POST', '/api/register', [
-            'name' => 'newUser',
-            'email' => 'newUser@example.org',
-            'password' => 'password',
-            'password_confirmation' => 'password',
-        ]);
-
-        $response->assertStatus(200)
-            ->assertJsonStructure([
-                'message' => ['token', 'name']
-            ]);
-    }
-
-}

+ 0 - 288
tests/Feature/ProtectDbTest.php

@@ -1,288 +0,0 @@
-<?php
-
-namespace Tests\Feature;
-
-use App\User;
-use Tests\TestCase;
-use App\TwoFAccount;
-use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Crypt;
-
-class ProtectDbTest extends TestCase
-{
-    /** @var \App\User, \App\TwoFAccount */
-    protected $user, $twofaccounts;
-
-
-    /**
-     * @test
-     */
-    public function setUp(): void
-    {
-        parent::setUp();
-
-        $this->user = factory(User::class)->create();
-        $this->twofaccount = factory(Twofaccount::class,)->create([
-            'service' => 'test',
-            'account' => 'test@test.com',
-            'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHVVRBGY7UIW&issuer=test',
-        ]);
-        $this->twofaccountAlt = factory(Twofaccount::class,)->create([
-            'service' => 'testAlt',
-            'account' => 'testAlt@test.com',
-            'uri' => 'otpauth://totp/testAlt@test.com?secret=A4GRFHVVRBGY7UIW&issuer=testAlt',
-        ]);
-    }
-
-
-    /**
-     * test db encryption via API
-     *
-     * @test
-     */
-    public function testDbEncryption()
-    {
-        // Encrypt db
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'useEncryption' => true,
-                ])
-            ->assertStatus(200);
-
-        // Get the raw encrypted records
-        $encrypted = DB::table('twofaccounts')->find($this->twofaccount->id);
-        $encryptedAlt = DB::table('twofaccounts')->find($this->twofaccountAlt->id);
-
-        // Get the accounts via API and check their consistency with raw data
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/' . $this->twofaccount->id)
-            ->assertStatus(200)
-            ->assertJsonFragment([
-                'service' => 'test',
-                'account' => Crypt::decryptString($encrypted->account),
-            ]);
-
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/' . $this->twofaccountAlt->id)
-            ->assertStatus(200)
-            ->assertJsonFragment([
-                'service' => 'testAlt',
-                'account' => Crypt::decryptString($encryptedAlt->account),
-            ]);
-    }
-
-
-    /**
-     * test Account update on protected DB via API
-     *
-     * @test
-     */
-    public function testTwofaccountUpdateOnProtectedDb()
-    {
-        // Encrypt db
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'useEncryption' => true,
-                ])
-            ->assertStatus(200);
-
-        // Only the Account field is encrypted
-        $response = $this->actingAs($this->user, 'api')
-            ->json('PUT', '/api/twofaccounts/' . $this->twofaccount->id, [
-                    'service' => 'testUpdate',
-                    'account' => 'testUpdate@test.com',
-                    'otpType' => 'totp',
-                    'secret' => 'A4GRFHVVRBGY7UIW',
-                    'secretIsBase32Encoded' => 1,
-                    'digits' => 8,
-                    'totpPeriod' => 30,
-                    'algorithm' => 'sha256',
-                ])
-            ->assertStatus(200)
-            ->assertJsonFragment([
-                'id' => 1,
-                'service' => 'testUpdate',
-                'account' => 'testUpdate@test.com',
-            ]);
-    }
-
-
-    /**
-     * test db encryption via API
-     *
-     * @test
-     */
-    public function testPreventDbEncryptionOnDbAlreadyEncrypted()
-    {
-        // Encrypt db
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'useEncryption' => true,
-                ]);
-
-        // Set the option again to force another encryption pass
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'useEncryption' => true,
-                ]);
-
-        // Get the account, it should be readable
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/' . $this->twofaccount->id)
-            ->assertStatus(200)
-            ->assertJsonFragment([
-                'service' => 'test',
-                'account' => 'test@test.com',
-            ]);
-
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/' . $this->twofaccountAlt->id)
-            ->assertStatus(200)
-            ->assertJsonFragment([
-                'service' => 'testAlt',
-                'account' => 'testAlt@test.com',
-            ]);
-    }
-
-
-    /**
-     * test db deciphering via API
-     *
-     * @test
-     */
-    public function testDbDeciphering()
-    {
-        // Encrypt db
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'useEncryption' => true,
-                ]);
-
-        // Decipher db
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'useEncryption' => false,
-                ])
-            ->assertStatus(200);
-
-        // Get the accounts, they should be readable
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/' . $this->twofaccount->id)
-            ->assertStatus(200)
-            ->assertJsonFragment([
-                'service' => 'test',
-                'account' => 'test@test.com',
-            ]);
-
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/' . $this->twofaccountAlt->id)
-            ->assertStatus(200)
-            ->assertJsonFragment([
-                'service' => 'testAlt',
-                'account' => 'testAlt@test.com',
-            ]);
-    }
-
-
-    /**
-     * test Protect DB option not being persisted if encryption fails via API
-     *
-     * @test
-     */
-    public function testAbortEncryptionIfSomethingGoesWrong()
-    {
-        // Set no APP_KEY to break Laravel encryption capability
-        config(['app.key' => '']);
-
-        // Decipher db
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'useEncryption' => true,
-                ])
-            ->assertStatus(400);
-
-        // Check ProtectDB option is not active
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/settings/options')
-            ->assertJsonFragment([
-                'useEncryption' => false
-            ]);
-    }
-
-
-    /**
-     * test Protect DB option not being persisted if decyphering fails via API
-     *
-     * @test
-     */
-    public function testAbortDecipheringIfSomethingGoesWrong()
-    {
-        // Encrypt db
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'useEncryption' => true,
-                ])
-            ->assertStatus(200);
-
-        // alter the ciphertext to make deciphering impossible
-        $affected = DB::table('twofaccounts')
-              ->where('id', 1)
-              ->update(['account' => 'xxxxxxxxx', 'uri' => 'yyyyyyyyy']);
-
-        // Decipher db
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'useEncryption' => false,
-                ])
-            ->assertStatus(400);
-
-        // Check ProtectDB option has been restored
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/settings/options')
-            ->assertJsonFragment([
-                'useEncryption' => true
-            ]);
-    }
-
-
-    /**
-     * test bad payload don't breaks anything via API
-     *
-     * @test
-     */
-    public function testBadPayloadDontBreakEncryptedAccountFetching()
-    {
-        // Encrypt db
-        $response = $this->actingAs($this->user, 'api')
-            ->json('POST', '/api/settings/options', [
-                    'useEncryption' => true,
-                ])
-            ->assertStatus(200);
-
-        // break the payload
-        DB::table('twofaccounts')
-            ->where('id', 1)
-            ->update([
-                'account' => 'YouShallNotPass',
-                'uri' => 'PasDeBrasPasDeChocolat',
-            ]);
-
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/1')
-            ->assertStatus(200)
-            ->assertJsonFragment([
-                'account' => '*encrypted*',
-                'service' => 'test',
-                'group_id' => null,
-                'isConsistent' => false,
-                'otpType' => null,
-                'digits' => null,
-                'hotpCounter' => null,
-                'totpPeriod' => null,
-            ])
-            ->assertJsonMissing([
-                'uri' => '*encrypted*',
-            ]);
-    }
-
-}

+ 5 - 5
tests/Unit/RouteTest.php → tests/Feature/RouteTest.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace Tests\Unit;
+namespace Tests\Feature;
 
-use Tests\TestCase;
+use Tests\FeatureTestCase;
 
-class RouteTest extends TestCase
+class RouteTest extends FeatureTestCase
 {
 
     /**
@@ -12,7 +12,7 @@ class RouteTest extends TestCase
      *
      * @test
      */
-    public function testLandingViewIsReturned()
+    public function test_landing_view_is_returned()
     {
         $response = $this->get(route('landing', ['any' => '/']));
 
@@ -25,7 +25,7 @@ class RouteTest extends TestCase
      *
      * @test
      */
-    public function testExceptionHandlerWithWebRoute()
+    public function test_exception_handler_with_web_route()
     {
         $response = $this->post('/');
 

+ 197 - 0
tests/Feature/Services/AppstractOptionsServiceTest.php

@@ -0,0 +1,197 @@
+<?php
+
+namespace Tests\Feature;
+
+use Tests\FeatureTestCase;
+use Illuminate\Support\Facades\DB;
+
+class AppstractOptionsServiceTest extends FeatureTestCase
+{
+    /**
+     * App\Services\SettingServiceInterface $settingService
+     */
+    protected $settingService;
+
+    private const KEY = 'key';
+    private const VALUE = 'value';
+    private const SETTING_NAME = 'MySetting';
+    private const SETTING_NAME_ALT = 'MySettingAlt';
+    private const SETTING_VALUE_STRING = 'MyValue';
+    private const SETTING_VALUE_TRUE_TRANSFORMED = '{{1}}';
+    private const SETTING_VALUE_FALSE_TRANSFORMED = '{{}}';
+    private const SETTING_VALUE_INT = 10;
+
+
+    /**
+     * @test
+     */
+    public function setUp() : void
+    {
+        parent::setUp();
+
+        $this->settingService = $this->app->make('App\Services\SettingServiceInterface');
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_get_string_setting_returns_correct_value()
+    {
+        DB::table('options')->insert(
+            [self::KEY => self::SETTING_NAME, self::VALUE => strval(self::SETTING_VALUE_STRING)]
+        );
+
+        $this->assertEquals(self::SETTING_VALUE_STRING, $this->settingService->get(self::SETTING_NAME));
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_get_boolean_setting_returns_true()
+    {
+        DB::table('options')->insert(
+            [self::KEY => self::SETTING_NAME, self::VALUE => strval(self::SETTING_VALUE_TRUE_TRANSFORMED)]
+        );
+
+        $this->assertEquals(true, $this->settingService->get(self::SETTING_NAME));
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_get_boolean_setting_returns_false()
+    {
+        DB::table('options')->insert(
+            [self::KEY => self::SETTING_NAME, self::VALUE => strval(self::SETTING_VALUE_FALSE_TRANSFORMED)]
+        );
+
+        $this->assertEquals(false, $this->settingService->get(self::SETTING_NAME));
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_get_int_setting_returns_int()
+    {
+        DB::table('options')->insert(
+            [self::KEY => self::SETTING_NAME, self::VALUE => strval(self::SETTING_VALUE_INT)]
+        );
+
+        $value = $this->settingService->get(self::SETTING_NAME);
+
+        $this->assertEquals(self::SETTING_VALUE_INT, $value);
+        $this->assertIsInt($value);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_all_returns_native_and_user_settings()
+    {
+        $native_options = config('2fauth.options');
+
+        DB::table('options')->insert(
+            [self::KEY => self::SETTING_NAME, self::VALUE => strval(self::SETTING_VALUE_STRING)]
+        );
+
+        $all = $this->settingService->all();
+
+        $this->assertCount(count($native_options)+1, $all);
+
+        $this->assertArrayHasKey(self::SETTING_NAME, $all);
+        $this->assertEquals($all[self::SETTING_NAME], self::SETTING_VALUE_STRING);
+
+        foreach ($native_options as $key => $val) {
+            $this->assertArrayHasKey($key, $all);
+            $this->assertEquals($all[$key], $val);
+        }
+
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_set_setting_persist_correct_value()
+    {
+        $value = $this->settingService->set(self::SETTING_NAME, self::SETTING_VALUE_STRING);
+
+        $this->assertDatabaseHas('options', [
+            self::KEY => self::SETTING_NAME,
+            self::VALUE => self::SETTING_VALUE_STRING
+        ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_set_array_of_settings_persist_correct_values()
+    {
+        $value = $this->settingService->set([
+            self::SETTING_NAME => self::SETTING_VALUE_STRING,
+            self::SETTING_NAME_ALT => self::SETTING_VALUE_INT,
+        ]);
+
+        $this->assertDatabaseHas('options', [
+            self::KEY => self::SETTING_NAME,
+            self::VALUE => self::SETTING_VALUE_STRING
+        ]);
+
+        $this->assertDatabaseHas('options', [
+            self::KEY => self::SETTING_NAME_ALT,
+            self::VALUE => self::SETTING_VALUE_INT
+        ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_set_true_setting_persist_transformed_boolean()
+    {
+        $value = $this->settingService->set(self::SETTING_NAME, true);
+
+        $this->assertDatabaseHas('options', [
+            self::KEY => self::SETTING_NAME,
+            self::VALUE => self::SETTING_VALUE_TRUE_TRANSFORMED
+        ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_set_false_setting_persist_transformed_boolean()
+    {
+        $value = $this->settingService->set(self::SETTING_NAME, false);
+
+        $this->assertDatabaseHas('options', [
+            self::KEY => self::SETTING_NAME,
+            self::VALUE => self::SETTING_VALUE_FALSE_TRANSFORMED
+        ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_del_remove_setting_from_db()
+    {
+        DB::table('options')->insert(
+            [self::KEY => self::SETTING_NAME, self::VALUE => strval(self::SETTING_VALUE_STRING)]
+        );
+
+        $value = $this->settingService->delete(self::SETTING_NAME);
+
+        $this->assertDatabaseMissing('options', [
+            self::KEY => self::SETTING_NAME,
+            self::VALUE => self::SETTING_VALUE_STRING
+        ]);
+    }
+}

+ 621 - 0
tests/Feature/Services/TwoFAccountServiceTest.php

@@ -0,0 +1,621 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\TwoFAccount;
+use Tests\FeatureTestCase;
+use Illuminate\Support\Facades\DB;
+
+
+/**
+ * @covers \App\Services\TwoFAccountService
+ */
+class TwoFAccountServiceTest extends FeatureTestCase
+{
+    /**
+     * App\Services\SettingServiceInterface $settingService
+     */
+    protected $twofaccountService;
+
+
+    /**
+     * App\TwoFAccount $customTotpTwofaccount
+     */
+    protected $customTotpTwofaccount;
+
+
+    /**
+     * App\TwoFAccount $customTotpTwofaccount
+     */
+    protected $customHotpTwofaccount;
+
+    private const ACCOUNT = 'account';
+    private const SERVICE = 'service';
+    private const SECRET = 'A4GRFHVVRBGY7UIW';
+    private const ALGORITHM_DEFAULT = 'sha1';
+    private const ALGORITHM_CUSTOM = 'sha256';
+    private const DIGITS_DEFAULT = 6;
+    private const DIGITS_CUSTOM = 7;
+    private const PERIOD_DEFAULT = 30;
+    private const PERIOD_CUSTOM = 40;
+    private const COUNTER_DEFAULT = 0;
+    private const COUNTER_CUSTOM = 5;
+    private const IMAGE = 'https%3A%2F%2Fen.opensuse.org%2Fimages%2F4%2F44%2FButton-filled-colour.png';
+    private const ICON = 'test.png';
+    private const TOTP_FULL_CUSTOM_URI = 'otpauth://totp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&period='.self::PERIOD_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
+    private const HOTP_FULL_CUSTOM_URI = 'otpauth://hotp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&counter='.self::COUNTER_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
+    private const TOTP_SHORT_URI = 'otpauth://totp/'.self::ACCOUNT.'?secret='.self::SECRET;
+    private const HOTP_SHORT_URI = 'otpauth://hotp/'.self::ACCOUNT.'?secret='.self::SECRET;
+    private const TOTP_URI_WITH_UNREACHABLE_IMAGE = 'otpauth://totp/service:account?secret=A4GRFHVVRBGY7UIW&image=https%3A%2F%2Fen.opensuse.org%2Fimage.png';
+    private const INVALID_OTPAUTH_URI = 'otpauth://Xotp/'.self::ACCOUNT.'?secret='.self::SECRET;
+    private const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP = [
+        'service'   => self::SERVICE,
+        'account'   => self::ACCOUNT,
+        'icon'      => self::ICON,
+        'otp_type'  => 'totp',
+        'secret'    => self::SECRET,
+        'digits'    => self::DIGITS_CUSTOM,
+        'algorithm' => self::ALGORITHM_CUSTOM,
+        'period'    => self::PERIOD_CUSTOM,
+        'counter'   => null,
+    ];
+    private const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP = [
+        'account'   => self::ACCOUNT,
+        'otp_type'  => 'totp',
+        'secret'    => self::SECRET,
+    ];
+    private const ARRAY_OF_PARAMETERS_FOR_UNSUPPORTED_OTP_TYPE = [
+        'account'   => self::ACCOUNT,
+        'otp_type'  => 'Xotp',
+        'secret'    => self::SECRET,
+    ];
+    private const ARRAY_OF_INVALID_PARAMETERS_FOR_TOTP = [
+        'account'   => self::ACCOUNT,
+        'otp_type'  => 'totp',
+        'secret'    => 0,
+    ];
+    private const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP = [
+        'service'   => self::SERVICE,
+        'account'   => self::ACCOUNT,
+        'icon'      => self::ICON,
+        'otp_type'  => 'hotp',
+        'secret'    => self::SECRET,
+        'digits'    => self::DIGITS_CUSTOM,
+        'algorithm' => self::ALGORITHM_CUSTOM,
+        'period'    => null,
+        'counter'   => self::COUNTER_CUSTOM,
+    ];
+    private const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP = [
+        'account'   => self::ACCOUNT,
+        'otp_type'  => 'hotp',
+        'secret'    => self::SECRET,
+    ];
+
+
+    /**
+     * @test
+     */
+    public function setUp() : void
+    {
+        parent::setUp();
+
+        $this->twofaccountService = $this->app->make('App\Services\TwoFAccountService');
+
+        $this->customTotpTwofaccount = new TwoFAccount;
+        $this->customTotpTwofaccount->legacy_uri = self::TOTP_FULL_CUSTOM_URI;
+        $this->customTotpTwofaccount->service = self::SERVICE;
+        $this->customTotpTwofaccount->account = self::ACCOUNT;
+        $this->customTotpTwofaccount->icon = self::ICON;
+        $this->customTotpTwofaccount->otp_type = 'totp';
+        $this->customTotpTwofaccount->secret = self::SECRET;
+        $this->customTotpTwofaccount->digits = self::DIGITS_CUSTOM;
+        $this->customTotpTwofaccount->algorithm = self::ALGORITHM_CUSTOM;
+        $this->customTotpTwofaccount->period = self::PERIOD_CUSTOM;
+        $this->customTotpTwofaccount->counter = null;
+        $this->customTotpTwofaccount->save();
+
+        $this->customHotpTwofaccount = new TwoFAccount;
+        $this->customHotpTwofaccount->legacy_uri = self::HOTP_FULL_CUSTOM_URI;
+        $this->customHotpTwofaccount->service = self::SERVICE;
+        $this->customHotpTwofaccount->account = self::ACCOUNT;
+        $this->customHotpTwofaccount->icon = self::ICON;
+        $this->customHotpTwofaccount->otp_type = 'hotp';
+        $this->customHotpTwofaccount->secret = self::SECRET;
+        $this->customHotpTwofaccount->digits = self::DIGITS_CUSTOM;
+        $this->customHotpTwofaccount->algorithm = self::ALGORITHM_CUSTOM;
+        $this->customHotpTwofaccount->period = null;
+        $this->customHotpTwofaccount->counter = self::COUNTER_CUSTOM;
+        $this->customHotpTwofaccount->save();
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_custom_totp_from_uri_returns_correct_value()
+    {
+        $twofaccount = $this->twofaccountService->createFromUri(self::TOTP_FULL_CUSTOM_URI);
+
+        $this->assertEquals('totp', $twofaccount->otp_type);
+        $this->assertEquals(self::TOTP_FULL_CUSTOM_URI, $twofaccount->legacy_uri);
+        $this->assertEquals(self::SERVICE, $twofaccount->service);
+        $this->assertEquals(self::ACCOUNT, $twofaccount->account);
+        $this->assertEquals(self::SECRET, $twofaccount->secret);
+        $this->assertEquals(self::DIGITS_CUSTOM, $twofaccount->digits);
+        $this->assertEquals(self::PERIOD_CUSTOM, $twofaccount->period);
+        $this->assertEquals(null, $twofaccount->counter);
+        $this->assertEquals(self::ALGORITHM_CUSTOM, $twofaccount->algorithm);
+        $this->assertStringEndsWith('.png',$twofaccount->icon);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_basic_totp_from_uri_returns_default_value()
+    {
+        $twofaccount = $this->twofaccountService->createFromUri(self::TOTP_SHORT_URI);
+
+        $this->assertEquals('totp', $twofaccount->otp_type);
+        $this->assertEquals(self::TOTP_SHORT_URI, $twofaccount->legacy_uri);
+        $this->assertEquals(self::ACCOUNT, $twofaccount->account);
+        $this->assertEquals(null, $twofaccount->service);
+        $this->assertEquals(self::SECRET, $twofaccount->secret);
+        $this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
+        $this->assertEquals(self::PERIOD_DEFAULT, $twofaccount->period);
+        $this->assertEquals(null, $twofaccount->counter);
+        $this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
+        $this->assertEquals(null, $twofaccount->icon);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_custom_hotp_from_uri_returns_correct_value()
+    {
+        $twofaccount = $this->twofaccountService->createFromUri(self::HOTP_FULL_CUSTOM_URI);
+
+        $this->assertEquals('hotp', $twofaccount->otp_type);
+        $this->assertEquals(self::HOTP_FULL_CUSTOM_URI, $twofaccount->legacy_uri);
+        $this->assertEquals(self::SERVICE, $twofaccount->service);
+        $this->assertEquals(self::ACCOUNT, $twofaccount->account);
+        $this->assertEquals(self::SECRET, $twofaccount->secret);
+        $this->assertEquals(self::DIGITS_CUSTOM, $twofaccount->digits);
+        $this->assertEquals(null, $twofaccount->period);
+        $this->assertEquals(self::COUNTER_CUSTOM, $twofaccount->counter);
+        $this->assertEquals(self::ALGORITHM_CUSTOM, $twofaccount->algorithm);
+        $this->assertStringEndsWith('.png',$twofaccount->icon);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_basic_hotp_from_uri_returns_default_value()
+    {
+        $twofaccount = $this->twofaccountService->createFromUri(self::HOTP_SHORT_URI);
+
+        $this->assertEquals('hotp', $twofaccount->otp_type);
+        $this->assertEquals(self::HOTP_SHORT_URI, $twofaccount->legacy_uri);
+        $this->assertEquals(null, $twofaccount->service);
+        $this->assertEquals(self::ACCOUNT, $twofaccount->account);
+        $this->assertEquals(self::SECRET, $twofaccount->secret);
+        $this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
+        $this->assertEquals(null, $twofaccount->period);
+        $this->assertEquals(self::COUNTER_DEFAULT, $twofaccount->counter);
+        $this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
+        $this->assertEquals(null, $twofaccount->icon);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_from_uri_persists_to_db()
+    {
+        $twofaccount = $this->twofaccountService->createFromUri(self::TOTP_SHORT_URI);
+
+        $this->assertDatabaseHas('twofaccounts', [
+            'otp_type'      => 'totp',
+            'legacy_uri'    => self::TOTP_SHORT_URI,
+            'service'       => null,
+            'account'       => self::ACCOUNT,
+            'secret'        => self::SECRET,
+            'digits'        => self::DIGITS_DEFAULT,
+            'period'        => self::PERIOD_DEFAULT,
+            'counter'       => null,
+            'algorithm'     => self::ALGORITHM_DEFAULT,
+            'icon'          => null,
+        ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_from_uri_does_not_persist_to_db()
+    {
+        $twofaccount = $this->twofaccountService->createFromUri(self::TOTP_SHORT_URI, false);
+
+        $this->assertDatabaseMissing('twofaccounts', [
+            'otp_type'      => 'totp',
+            'legacy_uri'    => self::TOTP_SHORT_URI,
+            'service'       => null,
+            'account'       => self::ACCOUNT,
+            'secret'        => self::SECRET,
+            'digits'        => self::DIGITS_DEFAULT,
+            'period'        => self::PERIOD_DEFAULT,
+            'counter'       => null,
+            'algorithm'     => self::ALGORITHM_DEFAULT,
+            'icon'          => null,
+        ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_from_invalid_uri_returns_ValidationException()
+    {
+        $this->expectException(\Illuminate\Validation\ValidationException::class);
+        $twofaccount = $this->twofaccountService->createFromUri(self::INVALID_OTPAUTH_URI);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_from_uri_without_label_returns_ValidationException()
+    {
+        $this->expectException(\Illuminate\Validation\ValidationException::class);
+        $twofaccount = $this->twofaccountService->createFromUri('otpauth://totp/?secret='.self::SECRET);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_custom_totp_from_parameters_returns_correct_value()
+    {
+        $twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP);
+
+        $this->assertEquals('totp', $twofaccount->otp_type);
+        $this->assertEquals(self::SERVICE, $twofaccount->service);
+        $this->assertEquals(self::ACCOUNT, $twofaccount->account);
+        $this->assertEquals(self::SECRET, $twofaccount->secret);
+        $this->assertEquals(self::DIGITS_CUSTOM, $twofaccount->digits);
+        $this->assertEquals(self::PERIOD_CUSTOM, $twofaccount->period);
+        $this->assertEquals(null, $twofaccount->counter);
+        $this->assertEquals(self::ALGORITHM_CUSTOM, $twofaccount->algorithm);
+        $this->assertStringEndsWith('.png',$twofaccount->icon);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_basic_totp_from_parameters_returns_correct_value()
+    {
+        $twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
+
+        $this->assertEquals('totp', $twofaccount->otp_type);
+        $this->assertEquals(null, $twofaccount->service);
+        $this->assertEquals(self::ACCOUNT, $twofaccount->account);
+        $this->assertEquals(self::SECRET, $twofaccount->secret);
+        $this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
+        $this->assertEquals(self::PERIOD_DEFAULT, $twofaccount->period);
+        $this->assertEquals(null, $twofaccount->counter);
+        $this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
+        $this->assertEquals(null, $twofaccount->icon);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_custom_hotp_from_parameters_returns_correct_value()
+    {
+        $twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP);
+
+        $this->assertEquals('hotp', $twofaccount->otp_type);
+        $this->assertEquals(self::SERVICE, $twofaccount->service);
+        $this->assertEquals(self::ACCOUNT, $twofaccount->account);
+        $this->assertEquals(self::SECRET, $twofaccount->secret);
+        $this->assertEquals(self::DIGITS_CUSTOM, $twofaccount->digits);
+        $this->assertEquals(null, $twofaccount->period);
+        $this->assertEquals(self::COUNTER_CUSTOM, $twofaccount->counter);
+        $this->assertEquals(self::ALGORITHM_CUSTOM, $twofaccount->algorithm);
+        $this->assertStringEndsWith('.png',$twofaccount->icon);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_basic_hotp_from_parameters_returns_correct_value()
+    {
+        $twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP);
+
+        $this->assertEquals('hotp', $twofaccount->otp_type);
+        $this->assertEquals(null, $twofaccount->service);
+        $this->assertEquals(self::ACCOUNT, $twofaccount->account);
+        $this->assertEquals(self::SECRET, $twofaccount->secret);
+        $this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
+        $this->assertEquals(null, $twofaccount->period);
+        $this->assertEquals(self::COUNTER_DEFAULT, $twofaccount->counter);
+        $this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
+        $this->assertEquals(null, $twofaccount->icon);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_from_parameters_persists_to_db()
+    {
+        $twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
+
+        $this->assertDatabaseHas('twofaccounts', [
+            'otp_type'      => 'totp',
+            'legacy_uri'    => self::TOTP_SHORT_URI,
+            'service'       => null,
+            'account'       => self::ACCOUNT,
+            'secret'        => self::SECRET,
+            'digits'        => self::DIGITS_DEFAULT,
+            'period'        => self::PERIOD_DEFAULT,
+            'counter'       => null,
+            'algorithm'     => self::ALGORITHM_DEFAULT,
+            'icon'          => null,
+        ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_create_from_parameters_does_not_persist_to_db()
+    {
+        $twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP, false);
+
+        $this->assertDatabaseMissing('twofaccounts', [
+            'otp_type'      => 'totp',
+            'legacy_uri'    => self::TOTP_SHORT_URI,
+            'service'       => null,
+            'account'       => self::ACCOUNT,
+            'secret'        => self::SECRET,
+            'digits'        => self::DIGITS_DEFAULT,
+            'period'        => self::PERIOD_DEFAULT,
+            'counter'       => null,
+            'algorithm'     => self::ALGORITHM_DEFAULT,
+            'icon'          => null,
+        ]);
+    }
+
+    
+    /**
+     * @test
+     */
+    public function test_create_from_unsupported_parameters_returns_ValidationException()
+    {
+        $this->expectException(\Illuminate\Validation\ValidationException::class);
+        $twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_PARAMETERS_FOR_UNSUPPORTED_OTP_TYPE);
+    }
+
+    
+    /**
+     * @test
+     */
+    public function test_create_from_invalid_parameters_type_returns_InvalidOtpParameterException()
+    {
+        $this->expectException(\App\Exceptions\InvalidOtpParameterException::class);
+        $twofaccount = $this->twofaccountService->createFromParameters([
+            'account'   => self::ACCOUNT,
+            'otp_type'  => 'totp',
+            'digits' => 'notsupported',
+        ]);
+    }
+
+    
+    /**
+     * @test
+     */
+    public function test_create_from_invalid_parameters_returns_InvalidOtpParameterException()
+    {
+        $this->expectException(\App\Exceptions\InvalidOtpParameterException::class);
+        $twofaccount = $this->twofaccountService->createFromParameters([
+            'account'   => self::ACCOUNT,
+            'otp_type'  => 'totp',
+            'algorithm' => 'notsupported',
+        ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_update_totp_returns_updated_model()
+    {
+        $twofaccount = $this->twofaccountService->update($this->customTotpTwofaccount, self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
+
+        $this->assertEquals('totp', $twofaccount->otp_type);
+        $this->assertEquals(null, $twofaccount->service);
+        $this->assertEquals(self::ACCOUNT, $twofaccount->account);
+        $this->assertEquals(self::SECRET, $twofaccount->secret);
+        $this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
+        $this->assertEquals(self::PERIOD_DEFAULT, $twofaccount->period);
+        $this->assertEquals(null, $twofaccount->counter);
+        $this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
+        $this->assertEquals(null, $twofaccount->counter);
+        $this->assertEquals(null, $twofaccount->icon);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_update_hotp_returns_updated_model()
+    {
+        $twofaccount = $this->twofaccountService->update($this->customTotpTwofaccount, self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP);
+
+        $this->assertEquals('hotp', $twofaccount->otp_type);
+        $this->assertEquals(null, $twofaccount->service);
+        $this->assertEquals(self::ACCOUNT, $twofaccount->account);
+        $this->assertEquals(self::SECRET, $twofaccount->secret);
+        $this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
+        $this->assertEquals(null, $twofaccount->period);
+        $this->assertEquals(self::COUNTER_DEFAULT, $twofaccount->counter);
+        $this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
+        $this->assertEquals(null, $twofaccount->counter);
+        $this->assertEquals(null, $twofaccount->icon);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_update_totp_persists_updated_model()
+    {
+        $twofaccount = $this->twofaccountService->update($this->customTotpTwofaccount, self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
+
+        $this->assertDatabaseHas('twofaccounts', [
+            'otp_type'      => 'totp',
+            'service'       => null,
+            'account'       => self::ACCOUNT,
+            'secret'        => self::SECRET,
+            'digits'        => self::DIGITS_DEFAULT,
+            'period'        => self::PERIOD_DEFAULT,
+            'counter'       => null,
+            'algorithm'     => self::ALGORITHM_DEFAULT,
+            'icon'          => null,
+        ]);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_getOTP_for_totp_returns_the_same_password()
+    {
+        $otp_from_model = $this->twofaccountService->getOTP($this->customTotpTwofaccount);
+        $otp_from_id = $this->twofaccountService->getOTP($this->customTotpTwofaccount->id);
+        $otp_from_uri = $this->twofaccountService->getOTP(self::TOTP_FULL_CUSTOM_URI);
+
+        // Those assertions may fail if the 3 previous assignments are not done at the same exact timestamp
+        $this->assertEquals($otp_from_model, $otp_from_id);
+        $this->assertEquals($otp_from_model, $otp_from_uri);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_getOTP_for_hotp_returns_the_same_password()
+    {
+        $otp_from_model = $this->twofaccountService->getOTP($this->customHotpTwofaccount);
+        $otp_from_id = $this->twofaccountService->getOTP($this->customHotpTwofaccount->id);
+        $otp_from_uri = $this->twofaccountService->getOTP(self::HOTP_FULL_CUSTOM_URI);
+
+        // Those assertions may fail if the 3 previous assignments are not done at the same exact timestamp
+        $this->assertEquals($otp_from_model, $otp_from_id);
+        $this->assertEquals($otp_from_model, $otp_from_uri);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_getOTP_for_totp_with_invalid_secret_returns_InvalidSecretException()
+    {
+        $this->expectException(\App\Exceptions\InvalidSecretException::class);
+        $otp_from_uri = $this->twofaccountService->getOTP('otpauth://totp/'.self::ACCOUNT.'?secret=0');
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_getURI_for_custom_totp_model_returns_uri()
+    {
+        $uri = $this->twofaccountService->getURI($this->customTotpTwofaccount);
+        
+        $this->assertStringContainsString('otpauth://totp/', $uri);
+        $this->assertStringContainsString(self::SERVICE, $uri);
+        $this->assertStringContainsString(self::ACCOUNT, $uri);
+        $this->assertStringContainsString('secret='.self::SECRET, $uri);
+        $this->assertStringContainsString('digits='.self::DIGITS_CUSTOM, $uri);
+        $this->assertStringContainsString('period='.self::PERIOD_CUSTOM, $uri);
+        $this->assertStringContainsString('algorithm='.self::ALGORITHM_CUSTOM, $uri);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_getURI_for_custom_totp_model_id_returns_uri()
+    {
+        $uri = $this->twofaccountService->getURI($this->customTotpTwofaccount->id);
+        
+        $this->assertStringContainsString('otpauth://totp/', $uri);
+        $this->assertStringContainsString(self::SERVICE, $uri);
+        $this->assertStringContainsString(self::ACCOUNT, $uri);
+        $this->assertStringContainsString('secret='.self::SECRET, $uri);
+        $this->assertStringContainsString('digits='.self::DIGITS_CUSTOM, $uri);
+        $this->assertStringContainsString('period='.self::PERIOD_CUSTOM, $uri);
+        $this->assertStringContainsString('algorithm='.self::ALGORITHM_CUSTOM, $uri);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_getURI_for_custom_hotp_model_returns_uri()
+    {
+        $uri = $this->twofaccountService->getURI($this->customHotpTwofaccount);
+        
+        $this->assertStringContainsString('otpauth://hotp/', $uri);
+        $this->assertStringContainsString(self::SERVICE, $uri);
+        $this->assertStringContainsString(self::ACCOUNT, $uri);
+        $this->assertStringContainsString('secret='.self::SECRET, $uri);
+        $this->assertStringContainsString('digits='.self::DIGITS_CUSTOM, $uri);
+        $this->assertStringContainsString('counter='.self::COUNTER_CUSTOM, $uri);
+        $this->assertStringContainsString('algorithm='.self::ALGORITHM_CUSTOM, $uri);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_getURI_for_custom_hotp_model_id_returns_uri()
+    {
+        $uri = $this->twofaccountService->getURI($this->customHotpTwofaccount->id);
+        
+        $this->assertStringContainsString('otpauth://hotp/', $uri);
+        $this->assertStringContainsString(self::SERVICE, $uri);
+        $this->assertStringContainsString(self::ACCOUNT, $uri);
+        $this->assertStringContainsString('secret='.self::SECRET, $uri);
+        $this->assertStringContainsString('digits='.self::DIGITS_CUSTOM, $uri);
+        $this->assertStringContainsString('counter='.self::COUNTER_CUSTOM, $uri);
+        $this->assertStringContainsString('algorithm='.self::ALGORITHM_CUSTOM, $uri);
+    }
+
+
+    /**
+     * @test
+     */
+    public function test_getURI_for_totp_dto_returns_uri()
+    {
+        $dto = new \App\Services\Dto\TwoFAccountDto;
+
+        $dto->otp_type = 'totp';
+        $dto->account = self::ACCOUNT;
+        $dto->secret = self::SECRET;
+
+        $uri = $this->twofaccountService->getURI($dto);
+        
+        $this->assertStringContainsString('otpauth://totp/', $uri);
+        $this->assertStringContainsString(self::ACCOUNT, $uri);
+        $this->assertStringContainsString('secret='.self::SECRET, $uri);
+    }
+
+}

+ 0 - 109
tests/Unit/ApiExceptionTest.php

@@ -1,109 +0,0 @@
-<?php
-
-namespace Tests\Unit;
-
-use App\User;
-use Tests\TestCase;
-use App\TwoFAccount;
-use App\Http\Controllers\TwoFAccountController;
-use Illuminate\Auth\Authenticatable;
-use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Hash;
-use Illuminate\Auth\RequestGuard;
-use Symfony\Component\HttpKernel\Exception\HttpException;
-
-class ApiExceptionTest extends TestCase
-{
-    /** @var \App\User */
-    protected $user;
-
-
-    /**
-     * @test
-     */
-    public function setUp(): void
-    {
-        parent::setUp();
-
-        $this->user = factory(User::class)->create();
-    }
-
-
-    /**
-     * test Unauthorized
-     *
-     * @test
-     */
-    public function testHttpUnauthenticated()
-    {
-        $response = $this->json('GET', '/api/settings/options')
-            ->assertStatus(401)
-            ->assertJson([
-                'message' => 'Unauthenticated.'
-            ]);
-    }
-
-
-    /**
-     * test Not Found
-     *
-     * @test
-     */
-    public function testHttpNotFound()
-    {
-        $response = $this->actingAs($this->user, 'api')
-            ->json('GET', '/api/twofaccounts/1000')
-            ->assertStatus(404);
-    }
-
-
-    /**
-     * test Method Not Allowed
-     *
-     * @test
-     */
-    public function testHttpMethodNotAllowed()
-    {
-        $response = $this->actingAs($this->user, 'api')
-            ->json('PATCH', '/api/settings/options')
-            ->assertStatus(405);
-    }
-
-
-    /**
-     * test Unprocessable entity
-     *
-     * @test
-     */
-    public function testHttpUnprocessableEntity()
-    {
-        $response = $this->json('POST', '/api/login')
-            ->assertStatus(422)
-            ->assertJsonStructure([
-                'message',
-                'errors'
-            ])
-            ->assertJsonValidationErrors([
-                'email',
-                'password'
-            ]);
-    }
-
-
-    /**
-     * test Internal Server error
-     *
-     * @test
-     */
-    public function testHttpInternalServerError()
-    {
-        factory(TwoFAccount::class, 3)->create();
-
-        $response = $this->actingAs($this->user, 'api')
-            ->json('PATCH', '/api/twofaccounts/reorder', [
-                'orderedIds' => 'x'])
-            ->assertStatus(500);
-            
-    }
-
-}

+ 0 - 84
tests/Unit/Settings/AccountTest.php

@@ -1,84 +0,0 @@
-<?php
-
-namespace Tests\Unit\Settings;
-
-use App\User;
-use Tests\TestCase;
-
-class AccountTest extends TestCase
-{
-    /** @var \App\User */
-    protected $user;
-
-
-    /**
-     * @test
-     */
-    public function setUp(): void
-    {
-        parent::setUp();
-
-        $this->user = factory(User::class)->create();
-    }
-
-
-    /**
-     * test Get user infos via API
-     *
-     * @test
-     */
-    public function testGetUserDetails()
-    {
-        $user = User::find(1);
-
-        $response = $this->actingAs($user, 'api')
-            ->json('GET', '/api/settings/account')
-            ->assertStatus(200)
-            ->assertJsonStructure(['name', 'email']);
-    }
-
-
-    /**
-     * test User update with wrong current password via API
-     *
-     * @test
-     */
-    public function testUserUpdateWithWrongCurrentPassword()
-    {
-        $user = User::find(1);
-
-        $response = $this->actingAs($user, 'api')
-            ->json('PATCH', '/api/settings/account', [
-                'name' => 'userUpdated',
-                'email' => 'userUpdated@example.org',
-                'password' => 'wrongPassword',
-            ]);
-
-        $response->assertStatus(400)
-            ->assertJsonStructure(['message']);
-    }
-
-
-    /**
-     * test User update via API
-     *
-     * @test
-     */
-    public function testUserUpdate()
-    {
-        $user = User::find(1);
-
-        $response = $this->actingAs($user, 'api')
-            ->json('PATCH', '/api/settings/account', [
-                'name' => 'userUpdated',
-                'email' => 'userUpdated@example.org',
-                'password' => 'password',
-            ]);
-
-        $response->assertStatus(200)
-            ->assertJsonFragment([
-                'username' => 'userUpdated'
-            ]);
-    }
-
-}

+ 0 - 67
tests/Unit/Settings/PasswordTest.php

@@ -1,67 +0,0 @@
-<?php
-
-namespace Tests\Unit\Settings;
-
-use App\User;
-use Tests\TestCase;
-use Illuminate\Support\Facades\Hash;
-
-class PasswordTest extends TestCase
-{
-    /** @var \App\User */
-    protected $user;
-
-
-    /**
-     * @test
-     */
-    public function setUp(): void
-    {
-        parent::setUp();
-
-        $this->user = factory(User::class)->create();
-    }
-
-
-    /**
-     * test User password update with wrong current password via API
-     *
-     * @test
-     */
-    public function testPasswordUpdateWithWrongCurrentPassword()
-    {        
-        $response = $this->actingAs($this->user, 'api')
-            ->json('PATCH', '/api/settings/password', [
-                'currentPassword' => 'wrongPassword',
-                'password' => 'passwordUpdated',
-                'password_confirmation' => 'passwordUpdated',
-            ]);
-
-        $response->assertStatus(400)
-            ->assertJsonStructure(['message']);
-    }
-
-
-    /**
-     * test User password update via API
-     *
-     * @test
-     */
-    public function testPasswordUpdate()
-    {
-        $response = $this->actingAs($this->user, 'api')
-            ->json('PATCH', '/api/settings/password', [
-                'currentPassword' => 'password',
-                'password' => 'passwordUpdated',
-                'password_confirmation' => 'passwordUpdated',
-            ]);
-
-        $response->assertStatus(200)
-            ->assertJsonStructure(['message']);
-
-        $this->user->refresh();
-
-        $this->assertTrue(Hash::check('passwordUpdated', $this->user->password));
-    }
-
-}