diff --git a/database/factories/NonLogUserFactory.php b/database/factories/NonLogUserFactory.php new file mode 100644 index 0000000..6e8401d --- /dev/null +++ b/database/factories/NonLogUserFactory.php @@ -0,0 +1,34 @@ + + */ +class NonLogUserFactory extends TestbenchUserFactory +{ + protected $model = NonLogUser::class; + + /** + * @return array{name: string, email: string, email_verified_at: \Illuminate\Support\Carbon, password: string, remember_token: string} + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => Hash::make('secret'), + 'remember_token' => Str::random(10), + ]; + } +} diff --git a/database/migrations/create_moonshine_change_logs_table.php b/database/migrations/create_moonshine_change_logs_table.php new file mode 100644 index 0000000..5712a66 --- /dev/null +++ b/database/migrations/create_moonshine_change_logs_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('moonshine_user_id'); + $table->morphs('changelogable'); + $table->longText('states_before'); + $table->longText('states_after'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('moonshine_change_logs'); + } +}; diff --git a/database/migrations/create_moonshine_users_table.php b/database/migrations/create_moonshine_users_table.php index e1eecb6..a276573 100644 --- a/database/migrations/create_moonshine_users_table.php +++ b/database/migrations/create_moonshine_users_table.php @@ -25,9 +25,6 @@ public function up(): void }); } - /** - * Reverse the migrations. - */ public function down(): void { Schema::dropIfExists('moonshine_users'); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 626acb5..9fe18bd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -24,3 +24,13 @@ parameters: message: "#^Parameter \\#2 \\$provider of class Jampire\\\\MoonshineImpersonate\\\\Guards\\\\SessionGuard constructor expects Illuminate\\\\Contracts\\\\Auth\\\\UserProvider, Illuminate\\\\Contracts\\\\Auth\\\\UserProvider\\|null given\\.$#" count: 1 path: src/ImpersonateServiceProvider.php + + - + message: "#^Call to an undefined method Illuminate\\\\Contracts\\\\Auth\\\\Authenticatable\\:\\:changeLogs\\(\\)\\.$#" + count: 1 + path: src/Listeners/LogImpersonationEnter.php + + - + message: "#^Call to an undefined method Illuminate\\\\Contracts\\\\Auth\\\\Authenticatable\\:\\:changeLogs\\(\\)\\.$#" + count: 1 + path: src/Listeners/LogImpersonationStopped.php diff --git a/src/Enums/Permission.php b/src/Enums/Permission.php index 68d9702..bff4e93 100644 --- a/src/Enums/Permission.php +++ b/src/Enums/Permission.php @@ -4,6 +4,9 @@ namespace Jampire\MoonshineImpersonate\Enums; +/** + * @author Dzianis Kotau + */ enum Permission: string { case IMPERSONATE = 'impersonate'; diff --git a/src/Enums/State.php b/src/Enums/State.php new file mode 100644 index 0000000..2770543 --- /dev/null +++ b/src/Enums/State.php @@ -0,0 +1,12 @@ + + */ +enum State: string +{ + case IMPERSONATION_ENTERED = 'impersonation_entered'; + case IMPERSONATION_STOPPED = 'impersonation_stopped'; +} diff --git a/src/ImpersonateServiceProvider.php b/src/ImpersonateServiceProvider.php index f937482..5f36980 100644 --- a/src/ImpersonateServiceProvider.php +++ b/src/ImpersonateServiceProvider.php @@ -11,6 +11,7 @@ use Jampire\MoonshineImpersonate\Actions\StopAction; use Jampire\MoonshineImpersonate\Guards\SessionGuard; use Jampire\MoonshineImpersonate\Http\Middleware\ImpersonateMiddleware; +use Jampire\MoonshineImpersonate\Providers\EventServiceProvider; use Jampire\MoonshineImpersonate\Services\ImpersonateManager; use Jampire\MoonshineImpersonate\Support\Settings; use Jampire\MoonshineImpersonate\UI\View\Components\StopImpersonation; @@ -39,6 +40,8 @@ public function register(): void fn (): StopAction => new StopAction(app(ImpersonateManager::class)) ); + $this->app->register(EventServiceProvider::class); + $this->mergeConfigFrom(__DIR__.'/../config/'.Settings::ALIAS.'.php', Settings::ALIAS); $this->registerAuthDriver(); diff --git a/src/Listeners/LogImpersonationEnter.php b/src/Listeners/LogImpersonationEnter.php new file mode 100644 index 0000000..08fc0da --- /dev/null +++ b/src/Listeners/LogImpersonationEnter.php @@ -0,0 +1,31 @@ + + */ +class LogImpersonationEnter implements ShouldQueue +{ + public function handle(ImpersonationEntered $event): void + { + $event->impersonated->changeLogs()->create([ + 'moonshine_user_id' => $event->impersonator->getAuthIdentifier(), + 'states_before' => State::IMPERSONATION_STOPPED->value, + 'states_after' => State::IMPERSONATION_ENTERED->value, + ]); + } + + public function shouldQueue(ImpersonationEntered $event): bool + { + return method_exists($event->impersonated, 'changeLogs'); + } +} diff --git a/src/Listeners/LogImpersonationStopped.php b/src/Listeners/LogImpersonationStopped.php new file mode 100644 index 0000000..754bde8 --- /dev/null +++ b/src/Listeners/LogImpersonationStopped.php @@ -0,0 +1,31 @@ + + */ +class LogImpersonationStopped implements ShouldQueue +{ + public function handle(ImpersonationStopped $event): void + { + $event->impersonated->changeLogs()->create([ + 'moonshine_user_id' => $event->impersonator->getAuthIdentifier(), + 'states_before' => State::IMPERSONATION_ENTERED->value, + 'states_after' => State::IMPERSONATION_STOPPED->value, + ]); + } + + public function shouldQueue(ImpersonationStopped $event): bool + { + return method_exists($event->impersonated, 'changeLogs'); + } +} diff --git a/src/Providers/EventServiceProvider.php b/src/Providers/EventServiceProvider.php new file mode 100644 index 0000000..f2481b8 --- /dev/null +++ b/src/Providers/EventServiceProvider.php @@ -0,0 +1,26 @@ + + */ +class EventServiceProvider extends ServiceProvider +{ + protected $listen = [ + ImpersonationEntered::class => [ + LogImpersonationEnter::class, + ], + ImpersonationStopped::class => [ + LogImpersonationStopped::class, + ], + ]; +} diff --git a/tests/Feature/Actions/EnterActionTest.php b/tests/Feature/Actions/EnterActionTest.php index 4c0ec1d..beb64bc 100644 --- a/tests/Feature/Actions/EnterActionTest.php +++ b/tests/Feature/Actions/EnterActionTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Support\Facades\Event; use Jampire\MoonshineImpersonate\Actions\EnterAction; use Jampire\MoonshineImpersonate\Support\Settings; use Jampire\MoonshineImpersonate\Tests\Stubs\Models\MoonshineUser; @@ -28,6 +29,8 @@ }); test('enter action validation works correctly', function (): void { + Event::fake(); + $user = User::factory()->create(); $moonShineUser = MoonshineUser::factory()->create(); actingAs($moonShineUser, Settings::moonShineGuard()); diff --git a/tests/Feature/Http/ImpersonateEnterTest.php b/tests/Feature/Http/ImpersonateEnterTest.php index d7d2be6..2ae05c3 100644 --- a/tests/Feature/Http/ImpersonateEnterTest.php +++ b/tests/Feature/Http/ImpersonateEnterTest.php @@ -18,6 +18,7 @@ test('privileged user can impersonate another user', function (): void { Event::fake(); + $user = User::factory()->create([ 'name' => 'user', ]); diff --git a/tests/Feature/Listeners/LogImpersonationEnterTest.php b/tests/Feature/Listeners/LogImpersonationEnterTest.php new file mode 100644 index 0000000..af907d1 --- /dev/null +++ b/tests/Feature/Listeners/LogImpersonationEnterTest.php @@ -0,0 +1,66 @@ +create(); + $moonShineUser = MoonshineUser::factory()->create(); + + assertDatabaseEmpty('moonshine_change_logs'); + + $event = new ImpersonationEntered($moonShineUser, $user); + $listener = new LogImpersonationEnter(); + $listener->handle($event); + + assertDatabaseHas('moonshine_change_logs', [ + 'moonshine_user_id' => $moonShineUser->getAuthIdentifier(), + 'changelogable_type' => $user::class, + 'changelogable_id' => $user->getKey(), + 'states_before' => '"'.State::IMPERSONATION_STOPPED->value.'"', + 'states_after' => '"'.State::IMPERSONATION_ENTERED->value.'"', + ]); +}); + +it('listens enter impersonation event', function (): void { + Event::fake(); + + $user = User::factory()->create(); + $moonShineUser = MoonshineUser::factory()->create(); + + actingAs($moonShineUser, Settings::moonShineGuard()); + post(route_impersonate('enter'), [ + 'id' => $user->id, + ])->assertSessionHasNoErrors(); + + Event::assertListening( + ImpersonationEntered::class, + LogImpersonationEnter::class + ); +}); + +it('cannot log enter impersonation mode without changeLogs relation', function (): void { + config(['auth.providers.users.model' => NonLogUser::class]); + + $user = NonLogUser::factory()->create(); + $moonShineUser = MoonshineUser::factory()->create(); + + $event = new ImpersonationEntered($moonShineUser, $user); + $listener = new LogImpersonationEnter(); + + expect($listener->shouldQueue($event)) + ->toBeFalse(); +}); diff --git a/tests/Feature/Listeners/LogImpersonationStoppedTest.php b/tests/Feature/Listeners/LogImpersonationStoppedTest.php new file mode 100644 index 0000000..9a13d90 --- /dev/null +++ b/tests/Feature/Listeners/LogImpersonationStoppedTest.php @@ -0,0 +1,67 @@ +create(); + $moonShineUser = MoonshineUser::factory()->create(); + + assertDatabaseEmpty('moonshine_change_logs'); + + $event = new ImpersonationStopped($moonShineUser, $user); + $listener = new LogImpersonationStopped(); + $listener->handle($event); + + assertDatabaseHas('moonshine_change_logs', [ + 'moonshine_user_id' => $moonShineUser->getAuthIdentifier(), + 'changelogable_type' => $user::class, + 'changelogable_id' => $user->getKey(), + 'states_before' => '"'.State::IMPERSONATION_ENTERED->value.'"', + 'states_after' => '"'.State::IMPERSONATION_STOPPED->value.'"', + ]); +}); + +it('listens stopped impersonation event', function (): void { + Event::fake(); + + $user = User::factory()->create(); + $moonShineUser = MoonshineUser::factory()->create(); + + actingAs($moonShineUser, Settings::moonShineGuard()) + ->withSession([Settings::key() => $user->getKey()]); + + get(route_impersonate('stop')) + ->assertSessionHasNoErrors(); + + Event::assertListening( + ImpersonationStopped::class, + LogImpersonationStopped::class + ); +}); + +it('cannot log stopped impersonation mode without changeLogs relation', function (): void { + config(['auth.providers.users.model' => NonLogUser::class]); + + $user = NonLogUser::factory()->create(); + $moonShineUser = MoonshineUser::factory()->create(); + + $event = new ImpersonationStopped($moonShineUser, $user); + $listener = new LogImpersonationStopped(); + + expect($listener->shouldQueue($event)) + ->toBeFalse(); +}); diff --git a/tests/Feature/UI/ItemActions/EnterImpersonationItemActionTest.php b/tests/Feature/UI/ItemActions/EnterImpersonationItemActionTest.php index 562271e..a7efdb6 100644 --- a/tests/Feature/UI/ItemActions/EnterImpersonationItemActionTest.php +++ b/tests/Feature/UI/ItemActions/EnterImpersonationItemActionTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Illuminate\Foundation\Auth\User as AuthUser; use Jampire\MoonshineImpersonate\Support\Settings; use Jampire\MoonshineImpersonate\Tests\Stubs\Models\MoonshineUser; use Jampire\MoonshineImpersonate\Tests\Stubs\Models\User; @@ -12,6 +13,9 @@ use function Pest\Laravel\actingAs; it('resolves correct item action class', function (): void { + // don't need to use User model here + config(['auth.providers.users.model' => AuthUser::class]); + $user = User::factory()->create(); $moonShineUser = MoonshineUser::factory()->create(); actingAs($moonShineUser, Settings::moonShineGuard()); diff --git a/tests/Stubs/Models/MoonshineChangeLog.php b/tests/Stubs/Models/MoonshineChangeLog.php new file mode 100644 index 0000000..842645e --- /dev/null +++ b/tests/Stubs/Models/MoonshineChangeLog.php @@ -0,0 +1,31 @@ + 'array', + 'states_after' => 'array', + ]; + + public function changelogable(): MorphTo + { + return $this->morphTo(); + } +} diff --git a/tests/Stubs/Models/NonLogUser.php b/tests/Stubs/Models/NonLogUser.php new file mode 100644 index 0000000..86fb577 --- /dev/null +++ b/tests/Stubs/Models/NonLogUser.php @@ -0,0 +1,26 @@ + + */ +class NonLogUser extends BaseUser +{ + use HasFactory; + + protected $table = 'users'; + + protected static function newFactory(): NonLogUserFactory + { + return NonLogUserFactory::new(); + } +} diff --git a/tests/Stubs/Models/User.php b/tests/Stubs/Models/User.php index e3a6f5a..ef703af 100644 --- a/tests/Stubs/Models/User.php +++ b/tests/Stubs/Models/User.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as BaseUser; use Jampire\MoonshineImpersonate\Database\Factories\UserFactory; +use MoonShine\Traits\Models\HasMoonShineChangeLog; /** * Class User @@ -16,6 +17,7 @@ class User extends BaseUser { use HasFactory; + use HasMoonShineChangeLog; protected $table = 'users'; diff --git a/tests/TestCase.php b/tests/TestCase.php index 722b01c..b4d88c7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,6 +7,7 @@ use Illuminate\Foundation\Testing\Concerns\InteractsWithViews; use Illuminate\Foundation\Testing\RefreshDatabase; use Jampire\MoonshineImpersonate\ImpersonateServiceProvider; +use Jampire\MoonshineImpersonate\Tests\Stubs\Models\User; use Orchestra\Testbench\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase @@ -20,6 +21,8 @@ protected function setUp(): void $this->loadLaravelMigrations(['--database' => 'testing']); $this->artisan('migrate', ['--database' => 'testing'])->run(); + + config(['auth.providers.users.model' => User::class]); } protected function getPackageProviders($app): array @@ -32,7 +35,9 @@ protected function getPackageProviders($app): array protected function getEnvironmentSetUp($app): void { include_once __DIR__.'/../database/migrations/create_moonshine_users_table.php'; + include_once __DIR__.'/../database/migrations/create_moonshine_change_logs_table.php'; (new \CreateMoonShineUsersTable())->up(); + (new \CreateMoonShineChangeLogsTable())->up(); } } diff --git a/tests/Unit/SettingsTest.php b/tests/Unit/SettingsTest.php index 006945f..47bd47c 100644 --- a/tests/Unit/SettingsTest.php +++ b/tests/Unit/SettingsTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use Illuminate\Foundation\Auth\User; use Jampire\MoonshineImpersonate\Support\Settings; +use Jampire\MoonshineImpersonate\Tests\Stubs\Models\User; it('uses correct User model', function (): void { expect(Settings::userClass())