From 1da59ca264b5cf5cacb0f7cb4e594a1961b9cf1b Mon Sep 17 00:00:00 2001 From: Son Tran <286.trants@gmail.com> Date: Tue, 2 Jul 2024 00:02:47 +0700 Subject: [PATCH] feat(native): Handles it all automatically Handles it all for you automatically, create roles, throttling and so much more! --- src/Laratrust.php | 928 +++++++++++++++++++++ src/Native/ConfigRepository.php | 81 ++ src/Native/Facades/Laratrust.php | 97 +++ src/Native/LaratrustBootstrapper.php | 335 ++++++++ tests/LaratrustTest.php | 823 ++++++++++++++++++ tests/Native/LaratrustBootstrapperTest.php | 25 + 6 files changed, 2289 insertions(+) create mode 100644 src/Laratrust.php create mode 100644 src/Native/ConfigRepository.php create mode 100644 src/Native/Facades/Laratrust.php create mode 100644 src/Native/LaratrustBootstrapper.php create mode 100644 tests/LaratrustTest.php create mode 100644 tests/Native/LaratrustBootstrapperTest.php diff --git a/src/Laratrust.php b/src/Laratrust.php new file mode 100644 index 0000000..21a2807 --- /dev/null +++ b/src/Laratrust.php @@ -0,0 +1,928 @@ +users = $users; + + $this->roles = $roles; + + $this->dispatcher = $dispatcher; + + $this->activations = $activations; + + $this->persistences = $persistences; + } + + /** + * Registers and activates the user. + * + * @param array $credentials + * + * @return bool|UserInterface + */ + public function registerAndActivate(array $credentials) + { + return $this->register($credentials, true); + } + + /** + * Registers a user. You may provide a callback to occur before the user + * is saved, or provide a true boolean as a shortcut to activation. + * + * @param array $credentials + * @param bool|\Closure $callback + * + * @throws \InvalidArgumentException + * + * @return bool|UserInterface + */ + public function register(array $credentials, $callback = false) + { + if (! $callback instanceof \Closure && ! is_bool($callback)) { + throw new \InvalidArgumentException('You must provide a closure or a boolean.'); + } + + $this->fireEvent('laratrust.registering', [$credentials]); + + $valid = $this->users->validForCreation($credentials); + + if (! $valid) { + return false; + } + + $argument = $callback instanceof \Closure ? $callback : null; + + $user = $this->users->create($credentials, $argument); + + if ($callback === true) { + $this->activate($user); + } + + $this->fireEvent('laratrust.registered', $user); + + return $user; + } + + /** + * Activates the given user. + * + * @param mixed $user + * + * @throws \InvalidArgumentException + * + * @return bool + */ + public function activate($user): bool + { + if (is_string($user) || is_array($user)) { + $users = $this->getUserRepository(); + + $method = 'findBy'.(is_string($user) ? 'Id' : 'Credentials'); + + $user = $users->{$method}($user); + } + + if (! $user instanceof UserInterface) { + throw new \InvalidArgumentException('No valid user was provided.'); + } + + $this->fireEvent('laratrust.activating', $user); + + $activations = $this->getActivationRepository(); + + $activation = $activations->create($user); + + $this->fireEvent('laratrust.activated', [$user, $activation]); + + return $activations->complete($user, $activation->getCode()); + } + + /** + * Returns the user repository. + * + * @return UserRepositoryInterface + */ + public function getUserRepository(): UserRepositoryInterface + { + return $this->users; + } + + /** + * Returns the activations repository. + * + * @return ActivationRepositoryInterface + */ + public function getActivationRepository(): ActivationRepositoryInterface + { + return $this->activations; + } + + /** + * Checks to see if a user is logged in, bypassing checkpoints. + * + * @return bool|UserInterface + */ + public function forceCheck() + { + return $this->bypassCheckpoints(function ($laratrust) { + return $laratrust->check(); + }); + } + + /** + * Pass a closure to Laratrust to bypass checkpoints. + * + * @param \Closure $callback + * @param array $checkpoints + * + * @return mixed + */ + public function bypassCheckpoints(\Closure $callback, array $checkpoints = []) + { + $originalCheckpoints = $this->checkpoints; + + $activeCheckpoints = []; + + foreach (array_keys($originalCheckpoints) as $checkpoint) { + if ($checkpoints && ! in_array($checkpoint, $checkpoints)) { + $activeCheckpoints[$checkpoint] = $originalCheckpoints[$checkpoint]; + } + } + + // Temporarily replace the registered checkpoints + $this->checkpoints = $activeCheckpoints; + + // Fire the callback + $result = $callback($this); + + // Reset checkpoints + $this->checkpoints = $originalCheckpoints; + + return $result; + } + + /** + * Checks to see if a user is logged in. + * + * @return bool|UserInterface + */ + public function check() + { + if ($this->user !== null) { + return $this->user; + } + + if (! $code = $this->persistences->check()) { + return false; + } + + if (! $user = $this->persistences->findUserByPersistenceCode($code)) { + return false; + } + + if (! $this->cycleCheckpoints('check', $user)) { + return false; + } + + return $this->user = $user; + } + + /** + * Cycles through all the registered checkpoints for a user. Checkpoints + * may throw their own exceptions, however, if just one returns false, + * the cycle fails. + * + * @param string $method + * @param UserInterface $user + * @param bool $halt + * + * @return bool + */ + protected function cycleCheckpoints(string $method, ?UserInterface $user = null, bool $halt = true): bool + { + if (! $this->checkpointsStatus) { + return true; + } + + foreach ($this->checkpoints as $checkpoint) { + $response = $checkpoint->{$method}($user); + + if (! $response && $halt) { + return false; + } + } + + return true; + } + + /** + * Checks if we are currently a guest. + * + * @return bool + */ + public function guest(): bool + { + return ! $this->check(); + } + + /** + * Authenticates a user, with the "remember" flag. + * + * @param array|UserInterface $credentials + * + * @return bool|UserInterface + */ + public function authenticateAndRemember($credentials) + { + return $this->authenticate($credentials, true); + } + + /** + * Authenticates a user, with "remember" flag. + * + * @param array|UserInterface $credentials + * @param bool $remember + * @param bool $login + * + * @return bool|UserInterface + */ + public function authenticate($credentials, bool $remember = false, bool $login = true) + { + $response = $this->fireEvent('laratrust.authenticating', [$credentials], true); + + if ($response === false) { + return false; + } + + if ($credentials instanceof UserInterface) { + $user = $credentials; + } else { + $user = $this->users->findByCredentials($credentials); + + $valid = $user !== null ? $this->users->validateCredentials($user, $credentials) : false; + + if (! $valid) { + $this->cycleCheckpoints('fail', $user, false); + + return false; + } + } + + if (! $this->cycleCheckpoints('login', $user)) { + return false; + } + + if ($login) { + if (! $user = $this->login($user, $remember)) { + return false; + } + } + + $this->fireEvent('laratrust.authenticated', $user); + + return $this->user = $user; + } + + /** + * Persists a login for the given user. + * + * @param UserInterface $user + * @param bool $remember + * + * @return bool|UserInterface + */ + public function login(UserInterface $user, bool $remember = false) + { + $this->fireEvent('laratrust.logging-in', $user); + + $this->persistences->persist($user, $remember); + + $response = $this->users->recordLogin($user); + + if (! $response) { + return false; + } + + $this->fireEvent('laratrust.logged-in', $user); + + return $this->user = $user; + } + + /** + * Forces an authentication to bypass checkpoints, with the "remember" flag. + * + * @param array|UserInterface $credentials + * + * @return bool|UserInterface + */ + public function forceAuthenticateAndRemember($credentials) + { + return $this->forceAuthenticate($credentials, true); + } + + /** + * Forces an authentication to bypass checkpoints. + * + * @param array|UserInterface $credentials + * @param bool $remember + * + * @return bool|UserInterface + */ + public function forceAuthenticate($credentials, bool $remember = false) + { + return $this->bypassCheckpoints(function ($laratrust) use ($credentials, $remember) { + return $laratrust->authenticate($credentials, $remember); + }); + } + + /** + * Attempt to authenticate using HTTP Basic Auth. + * + * @return mixed + */ + public function basic() + { + $credentials = $this->getRequestCredentials(); + + // We don't really want to add a throttling record for the + // first failed login attempt, which actually occurs when + // the user first hits a protected route. + if ($credentials === null) { + return $this->getBasicResponse(); + } + + $user = $this->stateless($credentials); + + if ($user) { + return; + } + + return $this->getBasicResponse(); + } + + /** + * Returns the request credentials. + * + * @return array|null + */ + public function getRequestCredentials(): ?array + { + if ($this->requestCredentials === null) { + $this->requestCredentials = function () { + $credentials = []; + + if (isset($_SERVER['PHP_AUTH_USER'])) { + $credentials['login'] = $_SERVER['PHP_AUTH_USER']; + } + + if (isset($_SERVER['PHP_AUTH_PW'])) { + $credentials['password'] = $_SERVER['PHP_AUTH_PW']; + } + + if (count($credentials) > 0) { + return $credentials; + } + }; + } + + $credentials = $this->requestCredentials; + + return $credentials(); + } + + /** + * Sets the closure which resolves the request credentials. + * + * @param \Closure $requestCredentials + * + * @return void + */ + public function setRequestCredentials(\Closure $requestCredentials): void + { + $this->requestCredentials = $requestCredentials; + } + + /** + * Sends a response when HTTP basic authentication fails. + * + * @throws \RuntimeException + * + * @return mixed + */ + public function getBasicResponse() + { + // Default the basic response + if ($this->basicResponse === null) { + $this->basicResponse = function () { + if (headers_sent()) { + throw new \RuntimeException('Attempting basic auth after headers have already been sent.'); + } + + header('WWW-Authenticate: Basic'); + header('HTTP/1.0 401 Unauthorized'); + + echo 'Invalid credentials.'; + exit; + }; + } + + $response = $this->basicResponse; + + return $response(); + } + + /** + * Attempt a stateless authentication. + * + * @param array|UserInterface $credentials + * + * @return bool|UserInterface + */ + public function stateless($credentials) + { + return $this->authenticate($credentials, false, false); + } + + /** + * Sets the callback which creates a basic response. + * + * @param \Closure $basicResonse + * + * @return void + */ + public function creatingBasicResponse(\Closure $basicResponse): void + { + $this->basicResponse = $basicResponse; + } + + /** + * Persists a login for the given user, with the "remember" flag. + * + * @param UserInterface $user + * + * @return bool|UserInterface + */ + public function loginAndRemember(UserInterface $user) + { + return $this->login($user, true); + } + + /** + * Logs the current user out. + * + * @param UserInterface|null $user + * @param bool $everywhere + * + * @return bool + */ + public function logout(?UserInterface $user = null, bool $everywhere = false): bool + { + $currentUser = $this->check(); + + $this->fireEvent('laratrust.logging-out', $user); + + if ($user && $user !== $currentUser) { + $this->persistences->flush($user, false); + + $this->fireEvent('laratrust.logged-out', $user); + + return true; + } + + $user = $user ?: $currentUser; + + if ($user === false) { + $this->fireEvent('laratrust.logged-out', $user); + + return true; + } + + $method = $everywhere === true ? 'flush' : 'forget'; + + $this->persistences->{$method}($user); + + $this->user = null; + + $this->fireEvent('laratrust.logged-out', $user); + + return $this->users->recordLogout($user); + } + + /** + * Checks if checkpoints are enabled. + * + * @return bool + */ + public function checkpointsStatus(): bool + { + return $this->checkpointsStatus; + } + + /** + * Enables checkpoints. + * + * @return void + */ + public function enableCheckpoints(): void + { + $this->checkpointsStatus = true; + } + + /** + * Disables checkpoints. + * + * @return void + */ + public function disableCheckpoints(): void + { + $this->checkpointsStatus = false; + } + + /** + * Returns all the added Checkpoints. + * + * @return array + */ + public function getCheckpoints(): array + { + return $this->checkpoints; + } + + /** + * Add a new checkpoint to Laratrust. + * + * @param string $key + * @param CheckpointInterface $checkpoint + * + * @return void + */ + public function addCheckpoint(string $key, CheckpointInterface $checkpoint): void + { + $this->checkpoints[$key] = $checkpoint; + } + + /** + * Removes the given checkpoints. + * + * @param array $checkpoints + * + * @return void + */ + public function removeCheckpoints(array $checkpoints = []): void + { + foreach ($checkpoints as $checkpoint) { + $this->removeCheckpoint($checkpoint); + } + } + + /** + * Removes a checkpoint. + * + * @param string $key + * + * @return void + */ + public function removeCheckpoint(string $key): void + { + if (isset($this->checkpoints[$key])) { + unset($this->checkpoints[$key]); + } + } + + /** + * Sets the user repository. + * + * @param UserRepositoryInterface $users + * + * @return void + */ + public function setUserRepository(UserRepositoryInterface $users): void + { + $this->users = $users; + + $this->userMethods = []; + } + + /** + * Sets the role repository. + * + * @param RoleRepositoryInterface $roles + * + * @return void + */ + public function setRoleRepository(RoleRepositoryInterface $roles): void + { + $this->roles = $roles; + } + + /** + * Returns the persistences repository. + * + * @return PersistenceRepositoryInterface + */ + public function getPersistenceRepository(): PersistenceRepositoryInterface + { + return $this->persistences; + } + + /** + * Sets the persistences repository. + * + * @param PersistenceRepositoryInterface $persistences + * + * @return void + */ + public function setPersistenceRepository(PersistenceRepositoryInterface $persistences): void + { + $this->persistences = $persistences; + } + + /** + * Sets the activations repository. + * + * @param ActivationRepositoryInterface $activations + * + * @return void + */ + public function setActivationRepository(ActivationRepositoryInterface $activations): void + { + $this->activations = $activations; + } + + /** + * Returns the reminders repository. + * + * @return ReminderRepositoryInterface + */ + public function getReminderRepository(): ReminderRepositoryInterface + { + return $this->reminders; + } + + /** + * Sets the reminders repository. + * + * @param ReminderRepositoryInterface $reminders + * + * @return void + */ + public function setReminderRepository(ReminderRepositoryInterface $reminders): void + { + $this->reminders = $reminders; + } + + /** + * Returns the throttle repository. + * + * @return ThrottleRepositoryInterface + */ + public function getThrottleRepository(): ThrottleRepositoryInterface + { + return $this->throttle; + } + + /** + * Sets the throttle repository. + * + * @param ThrottleRepositoryInterface $throttle + * + * @return void + */ + public function setThrottleRepository(ThrottleRepositoryInterface $throttle): void + { + $this->throttle = $throttle; + } + + /** + * Dynamically pass missing methods to Laratrust. + * + * @param string $method + * @param array $parameters + * + * @throws \BadMethodCallException + * + * @return mixed + */ + public function __call($method, $parameters) + { + $methods = $this->getUserMethods(); + + if (in_array($method, $methods)) { + $users = $this->getUserRepository(); + + return call_user_func_array([$users, $method], $parameters); + } + + if (Str::startsWith($method, 'findUserBy')) { + $user = $this->getUserRepository(); + + $method = 'findBy'.substr($method, 10); + + return call_user_func_array([$user, $method], $parameters); + } + + if (Str::startsWith($method, 'findRoleBy')) { + $roles = $this->getRoleRepository(); + + $method = 'findBy'.substr($method, 10); + + return call_user_func_array([$roles, $method], $parameters); + } + + $methods = ['getRoles', 'inRole', 'inAnyRole', 'hasAccess', 'hasAnyAccess']; + + $className = get_class($this); + + if (in_array($method, $methods)) { + $user = $this->getUser(); + + if ($user === null) { + throw new \BadMethodCallException("Method {$className}::{$method}() can only be called if a user is logged in."); + } + + return call_user_func_array([$user, $method], $parameters); + } + + throw new \BadMethodCallException("Call to undefined method {$className}::{$method}()"); + } + + /** + * Returns all accessible methods on the associated user repository. + * + * @return array + */ + protected function getUserMethods(): array + { + if (empty($this->userMethods)) { + $users = $this->getUserRepository(); + + $methods = get_class_methods($users); + + $this->userMethods = array_diff($methods, ['__construct']); + } + + return $this->userMethods; + } + + /** + * Returns the role repository. + * + * @return RoleRepositoryInterface + */ + public function getRoleRepository(): RoleRepositoryInterface + { + return $this->roles; + } + + /** + * Returns the currently logged-in user, lazily checking for it. + * + * @param bool $check + * + * @return UserInterface|null + */ + public function getUser(bool $check = true): ?UserInterface + { + if ($check && $this->user === null) { + $this->check(); + } + + return $this->user; + } + + /** + * Sets the user associated with Laratrust (does not log in). + * + * @param UserInterface $user + * + * @return void + */ + public function setUser(UserInterface $user) + { + $this->user = $user; + } +} diff --git a/src/Native/ConfigRepository.php b/src/Native/ConfigRepository.php new file mode 100644 index 0000000..4acec43 --- /dev/null +++ b/src/Native/ConfigRepository.php @@ -0,0 +1,81 @@ +file = $file ?: __DIR__.'/../config/config.php'; + + $this->load(); + } + + /** + * Load the configuration file. + * + * @return void + */ + protected function load() + { + $this->config = require $this->file; + } + + /** + * @inheritdoc + */ + public function offsetExists($key): bool + { + return isset($this->config[$key]); + } + + /** + * @inheritdoc + */ + public function offsetGet($key): mixed + { + return $this->config[$key]; + } + + /** + * @inheritdoc + */ + public function offsetSet($key, $value): void + { + $this->config[$key] = $value; + } + + /** + * @inheritdoc + */ + public function offsetUnset($key): void + { + unset($this->config[$key]); + } +} diff --git a/src/Native/Facades/Laratrust.php b/src/Native/Facades/Laratrust.php new file mode 100644 index 0000000..17821b8 --- /dev/null +++ b/src/Native/Facades/Laratrust.php @@ -0,0 +1,97 @@ +laratrust = $bootstrapper->createLaratrust(); + } + + /** + * Handle dynamic, static calls to the object. + * + * @param string $method + * @param array $args + * + * @return mixed + */ + public static function __callStatic($method, $args) + { + $instance = static::instance()->getLaratrust(); + + switch (count($args)) { + case 0: + return $instance->{$method}(); + case 1: + return $instance->{$method}($args[0]); + case 2: + return $instance->{$method}($args[0], $args[1]); + case 3: + return $instance->{$method}($args[0], $args[1], $args[2]); + case 4: + return $instance->{$method}($args[0], $args[1], $args[2], $args[3]); + default: + return call_user_func_array([$instance, $method], $args); + } + } + + /** + * Returns the Laratrust instance. + * + * @return \Focela\Laratrust\Laratrust + */ + public function getLaratrust() + { + return $this->laratrust; + } + + /** + * Creates a new Native Bootstraper instance. + * + * @param LaratrustBootstrapper $bootstrapper + * + * @return LaratrustBootstrapper + */ + public static function instance(?LaratrustBootstrapper $bootstrapper = null) + { + if (static::$instance === null) { + static::$instance = new static($bootstrapper); + } + + return static::$instance; + } +} diff --git a/src/Native/LaratrustBootstrapper.php b/src/Native/LaratrustBootstrapper.php new file mode 100644 index 0000000..0422083 --- /dev/null +++ b/src/Native/LaratrustBootstrapper.php @@ -0,0 +1,335 @@ +config = new ConfigRepository($config); + } else { + $this->config = $config ?: new ConfigRepository(); + } + } + + /** + * Creates a laratrust instance. + * + * @return Laratrust + */ + public function createLaratrust() + { + $persistence = $this->createPersistence(); + $users = $this->createUsers(); + $roles = $this->createRoles(); + $activations = $this->createActivations(); + $dispatcher = $this->getEventDispatcher(); + + $laratrust = new Laratrust( + $persistence, + $users, + $roles, + $activations, + $dispatcher + ); + + $throttle = $this->createThrottling(); + + $ipAddress = $this->getIpAddress(); + + $checkpoints = $this->createCheckpoints($activations, $throttle, $ipAddress); + + foreach ($checkpoints as $key => $checkpoint) { + $laratrust->addCheckpoint($key, $checkpoint); + } + + $reminders = $this->createReminders($users); + + $laratrust->setActivationRepository($activations); + + $laratrust->setReminderRepository($reminders); + + $laratrust->setThrottleRepository($throttle); + + return $laratrust; + } + + /** + * Creates a persistences repository. + * + * @return IlluminatePersistenceRepository + */ + protected function createPersistence() + { + $session = $this->createSession(); + + $cookie = $this->createCookie(); + + $model = $this->config['persistences']['model']; + + $single = $this->config['persistences']['single']; + + $users = $this->config['users']['model']; + + if (class_exists($users) && method_exists($users, 'setPersistencesModel')) { + forward_static_call_array([$users, 'setPersistencesModel'], [$model]); + } + + return new IlluminatePersistenceRepository($session, $cookie, $model, $single); + } + + /** + * Creates a session. + * + * @return NativeSession + */ + protected function createSession() + { + return new NativeSession($this->config['session']); + } + + /** + * Creates a cookie. + * + * @return NativeCookie + */ + protected function createCookie() + { + return new NativeCookie($this->config['cookie']); + } + + /** + * Creates a user repository. + * + * @return IlluminateUserRepository + */ + protected function createUsers() + { + $hasher = $this->createHasher(); + + $model = $this->config['users']['model']; + + $roles = $this->config['roles']['model']; + + $persistences = $this->config['persistences']['model']; + + if (class_exists($roles) && method_exists($roles, 'setUsersModel')) { + forward_static_call_array([$roles, 'setUsersModel'], [$model]); + } + + if (class_exists($persistences) && method_exists($persistences, 'setUsersModel')) { + forward_static_call_array([$persistences, 'setUsersModel'], [$model]); + } + + return new IlluminateUserRepository($hasher, $this->getEventDispatcher(), $model); + } + + /** + * Creates a hasher. + * + * @return NativeHasher + */ + protected function createHasher() + { + return new NativeHasher(); + } + + /** + * Returns the event dispatcher. + * + * @return \Illuminate\Contracts\Events\Dispatcher + */ + protected function getEventDispatcher() + { + if (! $this->dispatcher) { + $this->dispatcher = new Dispatcher(); + } + + return $this->dispatcher; + } + + /** + * Creates a role repository. + * + * @return IlluminateRoleRepository + */ + protected function createRoles() + { + $model = $this->config['roles']['model']; + + $users = $this->config['users']['model']; + + if (class_exists($users) && method_exists($users, 'setRolesModel')) { + forward_static_call_array([$users, 'setRolesModel'], [$model]); + } + + return new IlluminateRoleRepository($model); + } + + /** + * Creates an activation repository. + * + * @return IlluminateActivationRepository + */ + protected function createActivations() + { + $model = $this->config['activations']['model']; + + $expires = $this->config['activations']['expires']; + + return new IlluminateActivationRepository($model, $expires); + } + + /** + * Create a throttling repository. + * + * @return IlluminateThrottleRepository + */ + protected function createThrottling() + { + $model = $this->config['throttling']['model']; + + foreach (['global', 'ip', 'user'] as $type) { + ${"{$type}Interval"} = $this->config['throttling'][$type]['interval']; + + ${"{$type}Thresholds"} = $this->config['throttling'][$type]['thresholds']; + } + + return new IlluminateThrottleRepository( + $model, + $globalInterval, + $globalThresholds, + $ipInterval, + $ipThresholds, + $userInterval, + $userThresholds + ); + } + + /** + * Returns the client's ip address. + * + * @return string + */ + protected function getIpAddress() + { + $request = Request::createFromGlobals(); + + return $request->getClientIp(); + } + + /** + * Create activation and throttling checkpoints. + * + * @param IlluminateActivationRepository $activations + * @param IlluminateThrottleRepository $throttle + * @param string $ipAddress + * + * @throws \InvalidArgumentException + * + * @return array + */ + protected function createCheckpoints(IlluminateActivationRepository $activations, IlluminateThrottleRepository $throttle, $ipAddress) + { + $activeCheckpoints = $this->config['checkpoints']; + + $activation = $this->createActivationCheckpoint($activations); + + $throttle = $this->createThrottleCheckpoint($throttle, $ipAddress); + + $checkpoints = []; + + foreach ($activeCheckpoints as $checkpoint) { + if (! isset(${$checkpoint})) { + throw new \InvalidArgumentException("Invalid checkpoint [{$checkpoint}] given."); + } + + $checkpoints[$checkpoint] = ${$checkpoint}; + } + + return $checkpoints; + } + + /** + * Create an activation checkpoint. + * + * @param IlluminateActivationRepository $activations + * + * @return ActivationCheckpoint + */ + protected function createActivationCheckpoint(IlluminateActivationRepository $activations) + { + return new ActivationCheckpoint($activations); + } + + /** + * Create a throttle checkpoint. + * + * @param IlluminateThrottleRepository $throttle + * @param string $ipAddress + * + * @return ThrottleCheckpoint + */ + protected function createThrottleCheckpoint(IlluminateThrottleRepository $throttle, $ipAddress) + { + return new ThrottleCheckpoint($throttle, $ipAddress); + } + + /** + * Create a reminder repository. + * + * @param IlluminateUserRepository $users + * + * @return IlluminateReminderRepository + */ + protected function createReminders(IlluminateUserRepository $users) + { + $model = $this->config['reminders']['model']; + + $expires = $this->config['reminders']['expires']; + + return new IlluminateReminderRepository($users, $model, $expires); + } +} diff --git a/tests/LaratrustTest.php b/tests/LaratrustTest.php new file mode 100644 index 0000000..51e1c7e --- /dev/null +++ b/tests/LaratrustTest.php @@ -0,0 +1,823 @@ +users->shouldReceive('validForCreation')->once()->andReturn(true); + $this->users->shouldReceive('create')->once()->andReturn($this->user); + + $credentials = [ + 'email' => 'foo@example.com', + 'password' => 'secret', + ]; + + $this->dispatcher->shouldReceive('dispatch')->once() + ->with('laratrust.registering', [$credentials]) + ; + + $this->dispatcher->shouldReceive('dispatch')->once() + ->with('laratrust.registered', $this->user) + ; + + $result = $this->laratrust->register($credentials); + + $this->assertSame($result, $this->user); + } + + /** @test */ + public function it_can_register_and_activate_a_valid_user() + { + $this->users->shouldReceive('validForCreation')->once()->andReturn(true); + $this->users->shouldReceive('create')->once()->andReturn($this->user); + + $activation = m::mock(ActivationInterface::class); + $activation->shouldReceive('getCode')->once()->andReturn('a_random_code'); + + $this->activations->shouldReceive('create')->once()->andReturn($activation); + $this->activations->shouldReceive('complete')->once()->andReturn(true); + + $this->dispatcher->shouldReceive('dispatch')->times(4); + + $result = $this->laratrust->registerAndActivate([ + 'email' => 'foo@example.com', + 'password' => 'secret', + ]); + + $this->assertSame($result, $this->user); + } + + /** @test */ + public function it_will_not_register_an_invalid_user() + { + $this->users->shouldReceive('validForCreation')->once()->andReturn(false); + + $this->dispatcher->shouldReceive('dispatch')->once(); + + $result = $this->laratrust->register([ + 'email' => 'foo@example.com', + ]); + + $this->assertFalse($result); + } + + /** @test */ + public function it_can_activate_a_user_using_its_id() + { + $activation = m::mock(ActivationInterface::class); + $activation->shouldReceive('getCode')->once()->andReturn('a_random_code'); + + $this->users->shouldReceive('findById')->with('1')->once()->andReturn($this->user); + + $this->activations->shouldReceive('create')->once()->andReturn($activation); + $this->activations->shouldReceive('complete')->once()->andReturn(true); + + $this->dispatcher->shouldReceive('dispatch')->twice(); + + $this->assertTrue($this->laratrust->activate('1')); + } + + /** @test */ + public function it_can_activate_a_user_using_its_instance() + { + $activation = m::mock(ActivationInterface::class); + $activation->shouldReceive('getCode')->once()->andReturn('a_random_code'); + + $this->activations->shouldReceive('create')->once()->andReturn($activation); + $this->activations->shouldReceive('complete')->once()->andReturn(true); + + $this->dispatcher->shouldReceive('dispatch')->twice(); + + $this->assertTrue($this->laratrust->activate($this->user)); + } + + /** @test */ + public function it_can_activate_a_user_using_its_credentials() + { + $credentials = [ + 'login' => 'foo@example.com', + 'password' => 'secret', + ]; + + $activation = m::mock(ActivationInterface::class); + $activation->shouldReceive('getCode')->once()->andReturn('a_random_code'); + + $this->users->shouldReceive('findByCredentials')->with($credentials)->once()->andReturn($this->user); + + $this->activations->shouldReceive('create')->once()->andReturn($activation); + $this->activations->shouldReceive('complete')->once()->andReturn(true); + + $this->dispatcher->shouldReceive('dispatch')->twice(); + + $this->assertTrue($this->laratrust->activate($credentials)); + } + + /** @test */ + public function it_can_check_if_the_user_is_logged_in() + { + $this->persistences->shouldReceive('check')->once()->andReturn('foobar'); + $this->persistences->shouldReceive('findUserByPersistenceCode')->with('foobar')->andReturn($this->user); + + $this->assertSame($this->user, $this->laratrust->check()); + } + + /** @test */ + public function it_can_check_if_the_user_is_logged_in_when_it_is_not() + { + $this->persistences->shouldReceive('check')->once()->andReturn('foobar'); + $this->persistences->shouldReceive('findUserByPersistenceCode')->with('foobar')->andReturn(null); + + $this->assertFalse($this->laratrust->check()); + } + + /** @test */ + public function it_can_force_the_check_if_the_user_is_logged_in() + { + $this->persistences->shouldReceive('check')->once(); + $this->persistences->shouldReceive('findUserByPersistenceCode')->with('foobar')->andReturn($this->user); + + $checkpoint = m::mock(CheckpointInterface::class); + + $this->laratrust->addCheckpoint('activation', $checkpoint); + + $valid = $this->laratrust->forceCheck(); + + $this->assertFalse($valid); + } + + public function testGuest1() + { + $this->persistences->shouldReceive('check')->once(); + + $this->assertTrue($this->laratrust->guest()); + } + + public function testGuest2() + { + $this->laratrust->setUser($this->user); + + $this->assertFalse($this->laratrust->guest()); + } + + /** @test */ + public function it_can_authenticate_a_user_using_its_credentials() + { + $credentials = [ + 'login' => 'foo@example.com', + 'password' => 'secret', + ]; + + $this->persistences->shouldReceive('persist')->once(); + + $this->users->shouldReceive('findByCredentials')->with($credentials)->once()->andReturn($this->user); + $this->users->shouldReceive('validateCredentials')->once()->andReturn(true); + $this->users->shouldReceive('recordLogin')->once()->andReturn(true); + + $this->dispatcher->shouldReceive('until')->once() + ->with('laratrust.authenticating', [$credentials]) + ; + + $this->dispatcher->shouldReceive('dispatch')->once() + ->with('laratrust.logging-in', $this->user) + ; + + $this->dispatcher->shouldReceive('dispatch')->once() + ->with('laratrust.logged-in', $this->user) + ; + + $this->dispatcher->shouldReceive('dispatch')->once() + ->with('laratrust.authenticated', $this->user) + ; + + $this->assertSame($this->user, $this->laratrust->authenticate($credentials)); + } + + /** @test */ + public function it_can_authenticate_a_user_using_its_user_instance() + { + $this->persistences->shouldReceive('persist')->once(); + + $this->users->shouldReceive('recordLogin')->once()->andReturn(true); + + $this->dispatcher->shouldReceive('until')->once() + ->with('laratrust.authenticating', [$this->user]) + ; + + $this->dispatcher->shouldReceive('dispatch')->once() + ->with('laratrust.logging-in', $this->user) + ; + + $this->dispatcher->shouldReceive('dispatch')->once() + ->with('laratrust.logged-in', $this->user) + ; + + $this->dispatcher->shouldReceive('dispatch')->once() + ->with('laratrust.authenticated', $this->user) + ; + + $this->assertSame($this->user, $this->laratrust->authenticate($this->user)); + } + + /** @test */ + public function it_will_not_authenticate_a_user_with_invalid_credentials() + { + $this->users->shouldReceive('findByCredentials')->once(); + + $this->dispatcher->shouldReceive('until')->once(); + + $this->assertFalse($this->laratrust->authenticate([])); + } + + /** @test */ + public function it_can_authenticate_and_remember() + { + $credentials = [ + 'login' => 'foo@example.com', + 'password' => 'secret', + ]; + + $this->persistences->shouldReceive('persist')->once(); + + $this->users->shouldReceive('findByCredentials')->with($credentials)->once()->andReturn($this->user); + $this->users->shouldReceive('validateCredentials')->once()->andReturn(true); + $this->users->shouldReceive('recordLogin')->once()->andReturn(true); + + $this->dispatcher->shouldReceive('until')->once(); + $this->dispatcher->shouldReceive('dispatch')->times(3); + + $this->assertSame($this->user, $this->laratrust->authenticateAndRemember($credentials)); + } + + /** @test */ + public function it_can_authenticate_when_checkpoints_are_disabled() + { + $this->laratrust->disableCheckpoints(); + + $this->persistences->shouldReceive('persist')->once(); + $this->users->shouldReceive('recordLogin')->once()->andReturn(true); + + $this->dispatcher->shouldReceive('until'); + $this->dispatcher->shouldReceive('dispatch'); + + $this->assertSame($this->user, $this->laratrust->authenticate($this->user)); + } + + /** @test */ + public function it_cannot_authenticate_when_firing_an_event_fails() + { + $credentials = [ + 'login' => 'foo@example.com', + 'password' => 'secret', + ]; + + $this->dispatcher->shouldReceive('until')->once()->andReturn(false); + + $this->assertFalse($this->laratrust->authenticate($credentials)); + } + + /** @test */ + public function it_cannot_authenticate_when_a_checkpoint_fails() + { + $checkpoint = m::mock(CheckpointInterface::class); + $checkpoint->shouldReceive('login')->andReturn(false); + $this->laratrust->addCheckpoint('foobar', $checkpoint); + + $this->dispatcher->shouldReceive('until'); + $this->dispatcher->shouldReceive('dispatch'); + + $this->assertFalse($this->laratrust->authenticate($this->user)); + } + + /** @test */ + public function it_cannot_authenticate_when_a_login_fails() + { + $this->persistences->shouldReceive('persist')->once(); + + $this->users->shouldReceive('recordLogin')->once()->andReturn(false); + + $this->dispatcher->shouldReceive('until'); + $this->dispatcher->shouldReceive('dispatch'); + + $this->assertFalse($this->laratrust->authenticate($this->user)); + } + + /** @test */ + public function it_can_set_the_user_instance_on_the_laratrust_class() + { + $this->laratrust->setUser($this->user); + + $this->assertSame($this->user, $this->laratrust->getUser()); + } + + /** @test */ + public function it_can_bypass_all_checkpoints() + { + $this->persistences->shouldReceive('check')->once()->andReturn('foobar'); + $this->persistences->shouldReceive('findUserByPersistenceCode')->with('foobar')->andReturn($this->user); + + $activationCheckpoint = m::mock(CheckpointInterface::class); + $throttleCheckpoint = m::mock(CheckpointInterface::class); + + $this->laratrust->addCheckpoint('activation', $activationCheckpoint); + $this->laratrust->addCheckpoint('throttle', $throttleCheckpoint); + + $this->laratrust->bypassCheckpoints(function ($laratrust) { + $this->assertNotNull($laratrust->check()); + }); + } + + /** @test */ + public function it_can_bypass_a_specific_endpoint() + { + $this->persistences->shouldReceive('check')->once()->andReturn('foobar'); + $this->persistences->shouldReceive('findUserByPersistenceCode')->with('foobar')->andReturn($this->user); + + $activationCheckpoint = m::mock(CheckpointInterface::class); + + $throttleCheckpoint = m::mock(CheckpointInterface::class); + $throttleCheckpoint->shouldReceive('check')->once(); + + $this->laratrust->addCheckpoint('activation', $activationCheckpoint); + $this->laratrust->addCheckpoint('throttle', $throttleCheckpoint); + + $this->laratrust->bypassCheckpoints(function ($s) { + $this->assertNotNull($s->check()); + }, ['activation']); + } + + /** @test */ + public function it_can_get_the_checkpoint_status() + { + $this->laratrust->disableCheckpoints(); + + $this->assertFalse($this->laratrust->checkpointsStatus()); + + $this->laratrust->enableCheckpoints(); + + $this->assertTrue($this->laratrust->checkpointsStatus()); + } + + /** @test */ + public function it_can_disable_all_checkpoints() + { + $this->assertTrue($this->laratrust->checkpointsStatus()); + + $this->laratrust->disableCheckpoints(); + + $this->assertFalse($this->laratrust->checkpointsStatus()); + + $this->persistences->shouldReceive('check')->once()->andReturn('foobar'); + $this->persistences->shouldReceive('findUserByPersistenceCode')->with('foobar')->andReturn($this->user); + + $activationCheckpoint = m::mock(CheckpointInterface::class); + $throttleCheckpoint = m::mock(CheckpointInterface::class); + + $this->laratrust->addCheckpoint('activation', $activationCheckpoint); + $this->laratrust->addCheckpoint('throttle', $throttleCheckpoint); + + $this->assertNotNull($this->laratrust->check()); + } + + /** @test */ + public function it_can_enable_all_checkpoints() + { + $this->persistences->shouldReceive('check')->once()->andReturn('foobar'); + $this->persistences->shouldReceive('findUserByPersistenceCode')->with('foobar')->andReturn($this->user); + + $this->assertTrue($this->laratrust->checkpointsStatus()); + + $this->laratrust->disableCheckpoints(); + + $this->assertFalse($this->laratrust->checkpointsStatus()); + + $this->laratrust->enableCheckpoints(); + + $this->assertTrue($this->laratrust->checkpointsStatus()); + + $activationCheckpoint = m::mock(CheckpointInterface::class); + $throttleCheckpoint = m::mock(CheckpointInterface::class); + + $activationCheckpoint->shouldReceive('check')->once(); + + $this->laratrust->addCheckpoint('activation', $activationCheckpoint); + $this->laratrust->addCheckpoint('throttle', $throttleCheckpoint); + + $this->assertNotNull($this->laratrust->check()); + } + + /** @test */ + public function it_can_add_checkpoint_at_runtime() + { + $activationCheckpoint = m::mock(CheckpointInterface::class); + + $this->laratrust->addCheckpoint('activation', $activationCheckpoint); + + $this->assertCount(1, $this->laratrust->getCheckpoints()); + $this->assertArrayHasKey('activation', $this->laratrust->getCheckpoints()); + } + + /** @test */ + public function it_can_remove_checkpoint_at_runtime() + { + $activationCheckpoint = m::mock(CheckpointInterface::class); + $throttleCheckpoint = m::mock(CheckpointInterface::class); + + $this->laratrust->addCheckpoint('activation', $activationCheckpoint); + $this->laratrust->addCheckpoint('throttle', $throttleCheckpoint); + + $this->laratrust->removeCheckpoint('activation'); + + $this->assertCount(1, $this->laratrust->getCheckpoints()); + $this->assertArrayNotHasKey('activation', $this->laratrust->getCheckpoints()); + } + + /** @test */ + public function it_can_remove_checkpoints_at_runtime() + { + $activationCheckpoint = m::mock(CheckpointInterface::class); + $throttleCheckpoint = m::mock(CheckpointInterface::class); + + $this->laratrust->addCheckpoint('activation', $activationCheckpoint); + $this->laratrust->addCheckpoint('throttle', $throttleCheckpoint); + + $this->laratrust->removeCheckpoints([ + 'activation', + 'throttle', + ]); + + $this->assertCount(0, $this->laratrust->getCheckpoints()); + } + + /** @test */ + public function the_check_checkpoint_will_be_invoked() + { + $this->persistences->shouldReceive('check')->once()->andReturn('foobar'); + $this->persistences->shouldReceive('findUserByPersistenceCode')->with('foobar')->andReturn($this->user); + + $throttleCheckpoint = m::mock(CheckpointInterface::class); + $throttleCheckpoint->shouldReceive('check')->once()->andReturn(false); + + $this->laratrust->addCheckpoint('throttle', $throttleCheckpoint); + + $this->assertFalse($this->laratrust->check()); + } + + /** @test */ + public function the_login_checkpoint_will_be_invoked() + { + $this->dispatcher->shouldReceive('until')->once(); + + $throttleCheckpoint = m::mock(CheckpointInterface::class); + $throttleCheckpoint->shouldReceive('login')->once()->andReturn(false); + + $this->laratrust->addCheckpoint('throttle', $throttleCheckpoint); + + $this->assertFalse($this->laratrust->authenticate($this->user)); + } + + /** @test */ + public function the_fail_checkpoint_will_be_invoked() + { + $credentials = [ + 'login' => 'foo@example.com', + 'password' => 'secret', + ]; + + $this->dispatcher->shouldReceive('until')->once(); + + $this->users->shouldReceive('findByCredentials')->with($credentials)->once(); + + $throttleCheckpoint = m::mock(CheckpointInterface::class); + $throttleCheckpoint->shouldReceive('fail')->once()->andReturn(false); + + $this->laratrust->addCheckpoint('throttle', $throttleCheckpoint); + + $this->assertFalse($this->laratrust->authenticate($credentials)); + } + + /** @test */ + public function it_can_login_with_a_valid_user() + { + $this->persistences->shouldReceive('persist')->once(); + + $this->dispatcher->shouldReceive('dispatch')->twice(); + + $this->users->shouldReceive('recordLogin')->once()->andReturn(true); + + $this->assertSame($this->user, $this->laratrust->login($this->user)); + } + + /** @test */ + public function it_will_not_login_with_an_invalid_user() + { + $this->persistences->shouldReceive('persist')->once(); + + $this->dispatcher->shouldReceive('dispatch')->once(); + + $this->users->shouldReceive('recordLogin')->once()->andReturn(false); + + $this->assertFalse($this->laratrust->login($this->user)); + } + + public function it_will_ensure_the_user_is_not_defined_when_logging_out() + { + $this->persistences->shouldReceive('persist')->once(); + $this->persistences->shouldReceive('forget')->once(); + + $this->users->shouldReceive('recordLogin')->once(); + $this->users->shouldReceive('recordLogout')->once(); + + $this->laratrust->login($this->user); + $this->laratrust->logout($this->user); + + $this->assertNull($this->laratrust->getUser(false)); + } + + /** @test */ + public function it_can_logout_the_current_user() + { + $this->persistences->shouldReceive('check')->once()->andReturn('foobar'); + $this->persistences->shouldReceive('findUserByPersistenceCode')->with('foobar')->once()->andReturn($this->user); + $this->persistences->shouldReceive('forget')->once(); + + $this->users->shouldReceive('recordLogout')->once()->andReturn(true); + + $this->dispatcher->shouldReceive('dispatch')->twice(); + + $this->assertTrue($this->laratrust->logout($this->user)); + } + + /** @test */ + public function it_can_logout_the_user_on_the_other_devices() + { + $this->persistences->shouldReceive('check')->once()->andReturn('foobar'); + $this->persistences->shouldReceive('findUserByPersistenceCode')->with('foobar')->once()->andReturn($this->user); + $this->persistences->shouldReceive('flush')->once(); + + $this->dispatcher->shouldReceive('dispatch')->twice(); + + $this->users->shouldReceive('recordLogout')->once()->andReturn(true); + + $this->assertTrue($this->laratrust->logout($this->user, true)); + } + + /** @test */ + public function it_can_maintain_a_user_session_after_logging_out_another_user() + { + $currentUser = m::mock(EloquentUser::class); + + $this->persistences->shouldReceive('persist')->once(); + $this->persistences->shouldReceive('flush')->once()->with($this->user, false); + + $this->dispatcher->shouldReceive('dispatch')->times(4); + + $this->users->shouldReceive('recordLogin')->once()->andReturn(true); + + $this->laratrust->login($currentUser); + + $this->laratrust->logout($this->user); + + $this->assertSame($currentUser, $this->laratrust->getUser(false)); + } + + /** @test */ + public function it_can_logout_an_invalid_user() + { + $user = null; + + $this->persistences->shouldReceive('check')->once(); + + $this->dispatcher->shouldReceive('dispatch')->twice(); + + $this->assertTrue($this->laratrust->logout($user, true)); + } + + /** @test */ + public function it_can_create_a_basic_response() + { + $response = json_encode(['response']); + + $this->laratrust->creatingBasicResponse(function () use ($response) { + return $response; + }); + + $this->assertSame($response, $this->laratrust->getBasicResponse()); + } + + /** @test */ + public function it_can_set_and_get_the_various_repositories() + { + $this->laratrust->setPersistenceRepository($persistence = m::mock(PersistenceRepositoryInterface::class)); + $this->laratrust->setUserRepository($users = m::mock(UserRepositoryInterface::class)); + $this->laratrust->setRoleRepository($roles = m::mock(RoleRepositoryInterface::class)); + $this->laratrust->setActivationRepository($activations = m::mock(ActivationRepositoryInterface::class)); + $this->laratrust->setReminderRepository($reminders = m::mock(ReminderRepositoryInterface::class)); + $this->laratrust->setThrottleRepository($throttling = m::mock(ThrottleRepositoryInterface::class)); + + $this->assertSame($persistence, $this->laratrust->getPersistenceRepository()); + $this->assertSame($users, $this->laratrust->getUserRepository()); + $this->assertSame($roles, $this->laratrust->getRoleRepository()); + $this->assertSame($activations, $this->laratrust->getActivationRepository()); + $this->assertSame($reminders, $this->laratrust->getReminderRepository()); + $this->assertSame($throttling, $this->laratrust->getThrottleRepository()); + } + + /** @test */ + public function it_can_pass_method_calls_to_a_user_repository_directly() + { + $this->users->shouldReceive('findById')->once()->andReturn(m::mock(EloquentUser::class)); + + $user = $this->laratrust->findById(1); + + $this->assertInstanceOf(EloquentUser::class, $user); + } + + /** @test */ + public function it_can_pass_method_calls_to_a_user_repository_via_findUserBy() + { + $this->users->shouldReceive('findById')->once()->andReturn(m::mock(EloquentUser::class)); + + $user = $this->laratrust->findUserById(1); + + $this->assertInstanceOf(EloquentUser::class, $user); + } + + /** @test */ + public function it_can_pass_method_calls_to_a_role_repository_via_findRoleBy() + { + $this->roles->shouldReceive('findById')->once()->andReturn(m::mock(EloquentRole::class)); + + $user = $this->laratrust->findRoleById(1); + + $this->assertInstanceOf(EloquentRole::class, $user); + } + + /** @test */ + public function it_can_pass_methods_via_the_user_repository_when_a_user_is_logged_in() + { + $this->user->shouldReceive('hasAccess')->andReturn(true); + + $this->persistences->shouldReceive('check')->andReturn(true); + $this->persistences->shouldReceive('findUserByPersistenceCode')->andReturn($this->user); + + $this->assertTrue($this->laratrust->hasAccess()); + } + + /** @test */ + public function an_exception_will_be_thrown_when_activating_an_invalid_user() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('No valid user was provided.'); + + $this->laratrust->activate(20.00); + } + + /** @test */ + public function an_exception_will_be_thrown_when_registering_with_an_invalid_closure() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('You must provide a closure or a boolean.'); + + $this->laratrust->register([ + 'email' => 'foo@example.com', + ], 'invalid_closure'); + } + + /** @test */ + public function an_exception_will_be_thrown_when_calling_methods_which_are_only_available_when_a_user_is_logged_in() + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Method Focela\Laratrust\Laratrust::getRoles() can only be called if a user is logged in.'); + + $this->persistences->shouldReceive('check')->once()->andReturn(null); + + $this->laratrust->getRoles(); + } + + /** @test */ + public function an_exception_will_be_thrown_when_calling_invalid_methods() + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Call to undefined method Focela\Laratrust\Laratrust::methodThatDoesntExist()'); + + $this->laratrust->methodThatDoesntExist(); + } + + // /** @test */ + // public function an_exception_will_be_thrown_when_trying_to_get_the_basic_response() + // { + // $this->expectException(RuntimeException::class); + // $this->expectExceptionMessage('Attempting basic auth after headers have already been sent.'); + + // $this->laratrust->getBasicResponse(); + // } + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->user = m::mock(EloquentUser::class); + + $this->persistences = m::mock(PersistenceRepositoryInterface::class); + $this->users = m::mock(UserRepositoryInterface::class); + $this->roles = m::mock(RoleRepositoryInterface::class); + $this->activations = m::mock(ActivationRepositoryInterface::class); + $this->dispatcher = m::mock(Dispatcher::class); + + $this->laratrust = new Laratrust( + $this->persistences, + $this->users, + $this->roles, + $this->activations, + $this->dispatcher + ); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->user = null; + $this->laratrust = null; + $this->persistences = null; + $this->users = null; + $this->roles = null; + $this->activations = null; + $this->dispatcher = null; + m::close(); + } +} diff --git a/tests/Native/LaratrustBootstrapperTest.php b/tests/Native/LaratrustBootstrapperTest.php new file mode 100644 index 0000000..82a12d3 --- /dev/null +++ b/tests/Native/LaratrustBootstrapperTest.php @@ -0,0 +1,25 @@ +createLaratrust(); + + $this->assertInstanceOf(Laratrust::class, $laratrust); + } +}