From 5de02ab23a5df0a997b0e282d18991b12548f3dd Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 13:29:03 +0000 Subject: [PATCH] feat: rewrite auth to match new documentation --- .gitignore | 4 + alchemy.yml | 33 +++++ composer.json | 18 +-- phpunit.xml | 18 --- src/Auth.php | 152 ++++++++++------------- src/Auth/Core.php | 52 +++++--- src/Auth/Session.php | 7 +- src/Helpers/Authentication.php | 220 +++++++++++++++++---------------- src/Helpers/JWT.php | 28 ++--- src/functions.php | 2 +- tests/AuthSessionTest.php | 165 ------------------------- tests/AuthTest.php | 17 --- tests/Pest.php | 126 ++++++++----------- tests/auth.test.php | 152 +++++++++++++++++++++++ tests/extra.test.php | 27 ++++ tests/session.test.php | 183 +++++++++++++++++++++++++++ tests/table-actions.test.php | 92 ++++++++++++++ 17 files changed, 779 insertions(+), 517 deletions(-) create mode 100644 alchemy.yml delete mode 100644 phpunit.xml delete mode 100644 tests/AuthSessionTest.php delete mode 100644 tests/AuthTest.php create mode 100644 tests/auth.test.php create mode 100644 tests/extra.test.php create mode 100644 tests/session.test.php create mode 100644 tests/table-actions.test.php diff --git a/.gitignore b/.gitignore index 55e18db..21cd976 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ Thumbs.db # phpstorm .idea/* + +# Alchemy +.alchemy +.phpunit.result.cache diff --git a/alchemy.yml b/alchemy.yml new file mode 100644 index 0000000..3e79ea7 --- /dev/null +++ b/alchemy.yml @@ -0,0 +1,33 @@ +app: + - src + +tests: + engine: pest + parallel: true + paths: + - tests + files: + - '*.test.php' + coverage: + processUncoveredFiles: true + +lint: + preset: PSR12 + rules: + no_unused_imports: true + not_operator_with_successor_space: false + single_quote: true + +actions: + run: + - lint + - tests + os: + - ubuntu-latest + php: + extensions: json, zip, dom, curl, libxml, mbstring + versions: + - '8.3' + events: + - push + - pull_request diff --git a/composer.json b/composer.json index 661f552..280c38d 100644 --- a/composer.json +++ b/composer.json @@ -26,18 +26,19 @@ "Leaf\\": "src" }, "files": [ - "src/functions.php" - ] + "src/functions.php" + ] }, "minimum-stability": "stable", - "prefer-stable": true, + "prefer-stable": true, "require": { "leafs/date": "*", "leafs/password": "*", "leafs/session": "*", "leafs/db": "*", "leafs/form": "*", - "leafs/http": "*" + "leafs/http": "*", + "leafs/alchemy": "dev-next" }, "config": { "allow-plugins": { @@ -45,10 +46,13 @@ } }, "require-dev": { - "leafs/alchemy": "^1.0", - "pestphp/pest": "^1.0 | ^2.0" + "pestphp/pest": "^1.0 | ^2.0", + "friendsofphp/php-cs-fixer": "^3.64" }, "scripts": { - "test": "vendor/bin/pest --colors=always --coverage" + "test": "./vendor/bin/alchemy setup --test", + "alchemy": "./vendor/bin/alchemy setup", + "lint": "./vendor/bin/alchemy setup --lint", + "actions": "./vendor/bin/alchemy setup --actions" } } diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index 8f4b58c..0000000 --- a/phpunit.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - ./tests - - - - - ./app - ./src - - - diff --git a/src/Auth.php b/src/Auth.php index 386100e..aafa745 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -28,6 +28,7 @@ public static function login(array $credentials) { static::leafDbConnect(); + static::$errors = []; $table = static::$settings['db.table']; if (static::config('session')) { @@ -108,10 +109,11 @@ public static function login(array $credentials) * * @return array|false false or all user info + tokens + session data */ - public static function register(array $credentials, array $uniques = []) + public static function register(array $credentials) { static::leafDbConnect(); + static::$errors = []; $table = static::$settings['db.table']; $passKey = static::$settings['password.key']; @@ -123,6 +125,7 @@ public static function register(array $credentials, array $uniques = []) $credentials[$passKey] = (is_callable(static::$settings['password.encode'])) ? call_user_func(static::$settings['password.encode'], $credentials[$passKey]) : Password::hash($credentials[$passKey]); + } if (static::$settings['timestamps']) { @@ -131,14 +134,16 @@ public static function register(array $credentials, array $uniques = []) $credentials['updated_at'] = $now; } - if (static::$settings['id.uuid'] !== false) { - $credentials[static::$settings['id.key']] = call_user_func(static::$settings['id.uuid']); + if (isset($credentials[static::$settings['id.key']])) { + $credentials[static::$settings['id.key']] = is_callable($credentials[static::$settings['id.key']]) + ? call_user_func($credentials[static::$settings['id.key']]) + : $credentials[static::$settings['id.key']]; } try { - $query = static::$db->insert($table)->params($credentials)->unique($uniques)->execute(); + $query = static::$db->insert($table)->params($credentials)->unique(static::$settings['unique'])->execute(); } catch (\Throwable $th) { - trigger_error($th->getMessage()); + throw new \Exception($th->getMessage()); } if (!$query) { @@ -163,11 +168,16 @@ public static function register(array $credentials, array $uniques = []) $userId = $user[static::$settings['id.key']]; } - if (static::$settings['HIDE_ID']) { + if ( + in_array(static::$settings['id.key'], static::$settings['hidden']) || in_array('field.id', static::$settings['hidden']) + ) { unset($user[static::$settings['id.key']]); } - if (static::$settings['HIDE_PASSWORD'] && (isset($user[$passKey]) || !$user[$passKey])) { + if ( + (in_array(static::$settings['password.key'], static::$settings['hidden']) || in_array('field.password', static::$settings['hidden'])) + && (isset($user[$passKey]) || !$user[$passKey]) + ) { unset($user[$passKey]); } @@ -176,20 +186,14 @@ public static function register(array $credentials, array $uniques = []) return false; } - if (static::config('session')) { - if (static::config('SESSION_ON_REGISTER')) { - if (isset($userId)) { - $user[static::$settings['id.key']] = $userId; - } - - self::setUserToSession($user, $token); + if (static::config('session') && static::config('session.register')) { + static::useSession(); - exit(header('location: ' . static::config('GUARD_HOME'))); - } else { - if (static::config('SESSION_REDIRECT_ON_REGISTER')) { - exit(header('location: ' . static::config('GUARD_LOGIN'))); - } + if (isset($userId)) { + $user[static::$settings['id.key']] = $userId; } + + self::setUserToSession($user, $token); } $response['user'] = $user; @@ -206,10 +210,12 @@ public static function register(array $credentials, array $uniques = []) * * @return array|false all user info + tokens + session data */ - public static function update(array $credentials, array $uniques = []) + public static function update(array $credentials) { static::leafDbConnect(); + static::$errors = []; + $table = static::$settings['db.table']; if (static::config('session')) { @@ -227,28 +233,21 @@ public static function update(array $credentials, array $uniques = []) $where = isset($loggedInUser[static::$settings['id.key']]) ? [static::$settings['id.key'] => $loggedInUser[static::$settings['id.key']]] : $loggedInUser; if (!isset($credentials[$passKey])) { - static::$settings['password'] = true; + static::$settings['password'] = false; } - if ( - static::$settings['password'] === false && - static::$settings['password.encode'] !== false - ) { - if (is_callable(static::$settings['password.encode'])) { - $credentials[$passKey] = call_user_func(static::$settings['password.encode'], $credentials[$passKey]); - } else if (static::$settings['password.encode'] === 'md5') { - $credentials[$passKey] = md5($credentials[$passKey]); - } else { - $credentials[$passKey] = Password::hash($credentials[$passKey]); - } + if (static::$settings['password'] && static::$settings['password.encode'] !== false) { + $credentials[$passKey] = (is_callable(static::$settings['password.encode'])) + ? call_user_func(static::$settings['password.encode'], $credentials[$passKey]) + : Password::hash($credentials[$passKey]); } if (static::$settings['timestamps']) { $credentials['updated_at'] = (new \Leaf\Date())->tick()->format(static::$settings['timestamps.format']); } - if (count($uniques) > 0) { - foreach ($uniques as $unique) { + if (count(static::$settings['unique']) > 0) { + foreach (static::$settings['unique'] as $unique) { if (!isset($credentials[$unique])) { trigger_error("$unique not found in credentials."); } @@ -300,11 +299,17 @@ public static function update(array $credentials, array $uniques = []) $userId = $user[static::$settings['id.key']]; } - if (static::$settings['HIDE_ID'] && isset($user[static::$settings['id.key']])) { + if ( + (in_array(static::$settings['id.key'], static::$settings['hidden']) || in_array('field.id', static::$settings['hidden'])) + && isset($user[static::$settings['id.key']]) + ) { unset($user[static::$settings['id.key']]); } - if (static::$settings['HIDE_PASSWORD'] && (isset($user[$passKey]) || !$user[$passKey])) { + if ( + (in_array(static::$settings['password.key'], static::$settings['hidden']) || in_array('field.password', static::$settings['hidden'])) + && (isset($user[$passKey]) || !$user[$passKey]) + ) { unset($user[$passKey]); } @@ -318,14 +323,8 @@ public static function update(array $credentials, array $uniques = []) $user[static::$settings['id.key']] = $userId; } - static::$session->set('AUTH_USER', $user); - static::$session->set('HAS_SESSION', true); - - if (static::config('SAVE_SESSION_JWT')) { - static::$session->set('AUTH_TOKEN', $token); - } - - return $user; + static::$session->set('auth.user', $user); + static::$session->set('auth.token', $token); } $response['user'] = $user; @@ -340,7 +339,7 @@ public static function update(array $credentials, array $uniques = []) public static function useSession() { static::config('session', true); - static::$session = Auth\Session::init(static::config('SESSION_COOKIE_PARAMS')); + static::$session = Auth\Session::init(static::config('session.cookie')); } /** @@ -349,7 +348,7 @@ public static function useSession() protected static function sessionCheck() { if (!static::config('session')) { - trigger_error('Turn on session to use this feature.'); + trigger_error('Turn on sessions to use this feature.'); } if (!static::$session) { @@ -357,25 +356,6 @@ protected static function sessionCheck() } } - /** - * A simple auth guard: 'guest' pages can't be viewed when logged in, - * 'auth' pages can't be viewed without authentication - * - * @param string $type The type of guard/guard options - */ - public static function guard(string $type) - { - static::sessionCheck(); - - if ($type === 'guest' && static::status()) { - exit(header('location: ' . static::config('GUARD_HOME'), true, 302)); - } - - if ($type === 'auth' && !static::status()) { - exit(header('location: ' . static::config('GUARD_LOGIN'), true, 302)); - } - } - /** * Check session status */ @@ -384,7 +364,7 @@ public static function status() static::sessionCheck(); static::expireSession(); - return static::$session->get('AUTH_USER') ?? false; + return static::$session->get('auth.token') ?? false; } /** @@ -394,12 +374,14 @@ public static function id() { static::leafDbConnect(); + static::$errors = []; + if (static::config('session')) { if (static::expireSession()) { return null; } - return static::$session->get('AUTH_USER')[static::$settings['id.key']] ?? null; + return static::$session->get('auth.token')[static::$settings['id.key']] ?? null; } $payload = static::validateToken(static::config('token.secret')); @@ -417,11 +399,7 @@ public static function user(array $hidden = []) $table = static::$settings['db.table']; if (!static::id()) { - if (static::config('session')) { - return static::$session->get('AUTH_USER'); - } - - return null; + return (static::config('session')) ? static::$session->get('auth.token') : null; } $user = static::$db->select($table)->where(static::$settings['id.key'], static::id())->fetchAssoc(); @@ -463,7 +441,7 @@ private static function expireSession(): bool { self::sessionCheck(); - $sessionTtl = static::$session->get('SESSION_TTL'); + $sessionTtl = static::$session->get('session.ttl'); if (!$sessionTtl) { return false; @@ -472,12 +450,12 @@ private static function expireSession(): bool $isSessionExpired = time() > $sessionTtl; if ($isSessionExpired) { - static::$session->unset('AUTH_USER'); + static::$session->unset('auth.token'); static::$session->unset('HAS_SESSION'); - static::$session->unset('AUTH_TOKEN'); - static::$session->unset('SESSION_STARTED_AT'); - static::$session->unset('SESSION_LAST_ACTIVITY'); - static::$session->unset('SESSION_TTL'); + static::$session->unset('auth.token'); + static::$session->unset('session.startedAt'); + static::$session->unset('session.lastActivity'); + static::$session->unset('session.ttl'); } return $isSessionExpired; @@ -490,7 +468,7 @@ public static function lastActive() { static::sessionCheck(); - return time() - static::$session->get('SESSION_LAST_ACTIVITY'); + return time() - static::$session->get('session.lastActivity'); } /** @@ -504,8 +482,8 @@ public static function refresh(bool $clearData = true) $success = static::$session->regenerate($clearData); - static::$session->set('SESSION_STARTED_AT', time()); - static::$session->set('SESSION_LAST_ACTIVITY', time()); + static::$session->set('session.startedAt', time()); + static::$session->set('session.lastActivity', time()); static::setSessionTtl(); return $success; @@ -518,7 +496,7 @@ public static function length() { static::sessionCheck(); - return time() - static::$session->get('SESSION_STARTED_AT'); + return time() - static::$session->get('session.startedAt'); } /** @@ -531,12 +509,12 @@ private static function setUserToSession(array $user, string $token): void { session_regenerate_id(); - static::$session->set('AUTH_USER', $user); + static::$session->set('auth.token', $user); static::$session->set('HAS_SESSION', true); static::setSessionTtl(); if (static::config('SAVE_SESSION_JWT')) { - static::$session->set('AUTH_TOKEN', $token); + static::$session->set('auth.token', $token); } } @@ -545,14 +523,14 @@ private static function setUserToSession(array $user, string $token): void */ private static function setSessionTtl(): void { - $sessionLifetime = static::config('SESSION_LIFETIME'); + $sessionLifetime = static::config('session.lifetime'); if ($sessionLifetime === 0) { return; } if (is_int($sessionLifetime)) { - static::$session->set('SESSION_TTL', time() + $sessionLifetime); + static::$session->set('session.ttl', time() + $sessionLifetime); return; } @@ -562,6 +540,6 @@ private static function setSessionTtl(): void throw new \Exception('Provided string could not be converted to time'); } - static::$session->set('SESSION_TTL', $sessionLifetimeInTime); + static::$session->set('session.ttl', $sessionLifetimeInTime); } } diff --git a/src/Auth/Core.php b/src/Auth/Core.php index 1efad58..95fe009 100644 --- a/src/Auth/Core.php +++ b/src/Auth/Core.php @@ -27,8 +27,6 @@ class Core */ protected static $settings = [ 'id.key' => 'id', - 'id.uuid' => null, - 'db.table' => 'users', 'timestamps' => true, @@ -45,9 +43,9 @@ class Core 'session' => false, 'session.logout' => null, 'session.register' => null, - 'session.lifetime' => self::TIMESTAMP_OF_ONE_DAY, + 'session.lifetime' => 60 * 60 * 24, 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], - + 'token.lifetime' => null, 'token.secret' => '@_leaf$0Secret!', @@ -77,14 +75,20 @@ class Core */ public static function connect( $host, - string $dbname, - string $user, - string $password, - string $dbtype, + string $dbname = null, + string $user = null, + string $password = null, + string $dbtype = null, array $pdoOptions = [] ) { $db = new \Leaf\Db(); - $db->connect($host, $dbname, $user, $password, $dbtype, $pdoOptions); + + if (is_array($host)) { + $db->connect($host); + } else { + $db->connect($host, $dbname, $user, $password, $dbtype, $pdoOptions); + } + static::$db = $db; } @@ -97,6 +101,7 @@ public static function autoConnect(array $pdoOptions = []) { $db = new \Leaf\Db(); $db->autoConnect($pdoOptions); + static::$db = $db; } @@ -109,6 +114,7 @@ public static function dbConnection(\PDO $connection) { $db = new \Leaf\Db(); $db->connection($connection); + static::$db = $db; } @@ -157,12 +163,14 @@ public static function config($config, $value = null) */ public static function validateUserToken(string $token, ?string $secretKey = null) { - $payload = Authentication::validate($token, $secretKey ?? static::config("token.secret")); - if ($payload) return $payload; + $payload = Authentication::validate($token, $secretKey ?? static::config('token.secret')); - static::$errors = array_merge(static::$errors, Authentication::errors()); + if (!$payload) { + static::$errors = array_merge(static::$errors, Authentication::errors()); + return null; + } - return null; + return $payload; } /** @@ -172,12 +180,14 @@ public static function validateUserToken(string $token, ?string $secretKey = nul */ public static function validateToken(?string $secretKey = null) { - $payload = Authentication::validateToken($secretKey ?? static::config("token.secret")); - if ($payload) return $payload; + $payload = Authentication::validateToken($secretKey ?? static::config('token.secret')); - static::$errors = array_merge(static::$errors, Authentication::errors()); + if (!$payload) { + static::$errors = array_merge(static::$errors, Authentication::errors()); + return null; + } - return null; + return $payload; } /** @@ -186,11 +196,13 @@ public static function validateToken(?string $secretKey = null) public static function getBearerToken() { $token = Authentication::getBearerToken(); - if ($token) return $token; - static::$errors = array_merge(static::$errors, Authentication::errors()); + if (!$token) { + static::$errors = array_merge(static::$errors, Authentication::errors()); + return null; + } - return null; + return $token; } /** diff --git a/src/Auth/Session.php b/src/Auth/Session.php index e8da0d7..a80b735 100644 --- a/src/Auth/Session.php +++ b/src/Auth/Session.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace Leaf\Auth; + /** * Auth Sessions [CORE] * ----- @@ -28,11 +29,11 @@ public static function init(array $sessionCookieParams = []) session_start(); }; - if (!static::$session->get("SESSION_STARTED_AT")) { - static::$session->set("SESSION_STARTED_AT", time()); + if (!static::$session->get('session.startedAt')) { + static::$session->set('session.startedAt', time()); } - static::$session->set("SESSION_LAST_ACTIVITY", time()); + static::$session->set('session.lastActivity', time()); return static::$session; } diff --git a/src/Helpers/Authentication.php b/src/Helpers/Authentication.php index 5df9384..6cd01f4 100755 --- a/src/Helpers/Authentication.php +++ b/src/Helpers/Authentication.php @@ -6,37 +6,37 @@ * Leaf Authentication * --------------------------------------------- * Authentication helper for Leaf PHP - * + * * @author Michael Darko * @since v1.2.0 */ class Authentication { - /** - * Any errors caught - */ - protected static $errorsArray = []; - - /** - * Quickly generate a JWT encoding a user id - * - * @param string $userId The user id to encode - * @param string $secretPhrase The user id to encode - * @param int $expiresAt Token lifetime - * - * @return string The generated token - */ - public static function generateSimpleToken(string $userId, string $secretPhrase, int $expiresAt = null): string + /** + * Any errors caught + */ + protected static $errorsArray = []; + + /** + * Quickly generate a JWT encoding a user id + * + * @param string $userId The user id to encode + * @param string $secretPhrase The user id to encode + * @param int $expiresAt Token lifetime + * + * @return string The generated token + */ + public static function generateSimpleToken(string $userId, string $secretPhrase, int $expiresAt = null): string { - $payload = [ - 'iat' => time(), - 'iss' => 'localhost', - 'exp' => time() + ($expiresAt ?? (60 * 60 * 24)), - 'user_id' => $userId - ]; + $payload = [ + 'iat' => time(), + 'iss' => 'localhost', + 'exp' => time() + ($expiresAt ?? (60 * 60 * 24)), + 'user_id' => $userId + ]; - return self::generateToken($payload, $secretPhrase); - } + return self::generateToken($payload, $secretPhrase); + } /** * Create a JWT with your own payload @@ -46,89 +46,93 @@ public static function generateSimpleToken(string $userId, string $secretPhrase, * * @return string The generated token */ - public static function generateToken(array $payload, string $secretPhrase): string + public static function generateToken(array $payload, string $secretPhrase): string + { + return JWT::encode($payload, $secretPhrase); + } + + /** + * Get Authorization Headers + */ + public static function getAuthorizationHeader() + { + $headers = null; + + if (isset($_SERVER['Authorization'])) { + $headers = trim($_SERVER['Authorization']); + } elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) { + $headers = trim($_SERVER['HTTP_AUTHORIZATION']); + } elseif (function_exists('apache_request_headers')) { + $requestHeaders = apache_request_headers(); + // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) + $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); + + if (isset($requestHeaders['Authorization'])) { + $headers = trim($requestHeaders['Authorization']); + } + } + + return $headers; + } + + /** + * get access token from header + */ + public static function getBearerToken() + { + $headers = self::getAuthorizationHeader(); + + if (!empty($headers)) { + if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) { + return $matches[1]; + } + + self::$errorsArray['token'] = 'Access token not found'; + return null; + } + + self::$errorsArray['token'] = 'Access token not found'; + return null; + } + + /** + * Validate and decode access token in header + */ + public static function validateToken($secretPhrase) + { + $bearerToken = self::getBearerToken(); + if ($bearerToken === null) { + return null; + } + + return self::validate($bearerToken, $secretPhrase); + } + + /** + * Validate access token + * + * @param string $token Access token to validate and decode + */ + public static function validate($token, $secretPhrase) + { + try { + $payload = JWT::decode($token, $secretPhrase, ['HS256']); + if ($payload !== null) { + return $payload; + } + } catch (\DomainException $exception) { + self::$errorsArray['token'] = 'Malformed token'; + } + + self::$errorsArray = array_merge(self::$errorsArray, JWT::errors()); + return null; + } + + /** + * Get all authentication errors as associative array + */ + public static function errors() { - return JWT::encode($payload, $secretPhrase); - } - - /** - * Get Authorization Headers - */ - public static function getAuthorizationHeader() - { - $headers = null; - - if (isset($_SERVER['Authorization'])) { - $headers = trim($_SERVER["Authorization"]); - } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { - $headers = trim($_SERVER["HTTP_AUTHORIZATION"]); - } else if (function_exists('apache_request_headers')) { - $requestHeaders = apache_request_headers(); - // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) - $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); - - if (isset($requestHeaders['Authorization'])) { - $headers = trim($requestHeaders['Authorization']); - } - } - - return $headers; - } - - /** - * get access token from header - */ - public static function getBearerToken() - { - $headers = self::getAuthorizationHeader(); - - if (!empty($headers)) { - if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) { - return $matches[1]; - } - - self::$errorsArray["token"] = "Access token not found"; - return null; - } - - self::$errorsArray["token"] = "Access token not found"; - return null; - } - - /** - * Validate and decode access token in header - */ - public static function validateToken($secretPhrase) - { - $bearerToken = self::getBearerToken(); - if ($bearerToken === null) return null; - - return self::validate($bearerToken, $secretPhrase); - } - - /** - * Validate access token - * - * @param string $token Access token to validate and decode - */ - public static function validate($token, $secretPhrase) - { - try { - $payload = JWT::decode($token, $secretPhrase, ['HS256']); - if ($payload !== null) return $payload; - } catch (\DomainException $exception) { - self::$errorsArray["token"] = "Malformed token"; - } - - self::$errorsArray = array_merge(self::$errorsArray, JWT::errors()); - return null; - } - - /** - * Get all authentication errors as associative array - */ - public static function errors() - { - return self::$errorsArray; - } + return self::$errorsArray; + } } diff --git a/src/Helpers/JWT.php b/src/Helpers/JWT.php index 8fe1da5..fe48a98 100755 --- a/src/Helpers/JWT.php +++ b/src/Helpers/JWT.php @@ -60,30 +60,30 @@ public static function decode($jwt, $key, array $allowed_algs = array()) { $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; if (empty($key)) { - return static::saveErr("Key may not be empty"); + return static::saveErr('Key may not be empty'); } $tks = explode('.', $jwt); if (count($tks) != 3) { - return static::saveErr("Wrong number of segments"); + return static::saveErr('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { - return static::saveErr("Invalid header encoding"); + return static::saveErr('Invalid header encoding'); } if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { - return static::saveErr("Invalid claims encoding"); + return static::saveErr('Invalid claims encoding'); } if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { - return static::saveErr("Invalid signature encoding"); + return static::saveErr('Invalid signature encoding'); } if (empty($header->alg)) { - return static::saveErr("Empty algorithm"); + return static::saveErr('Empty algorithm'); } if (empty(static::$supported_algs[$header->alg])) { - return static::saveErr("Algorithm not supported"); + return static::saveErr('Algorithm not supported'); } if (!in_array($header->alg, $allowed_algs)) { - return static::saveErr("Algorithm not allowed"); + return static::saveErr('Algorithm not allowed'); } if (is_array($key) || $key instanceof \ArrayAccess) { if (isset($header->kid)) { @@ -97,13 +97,13 @@ public static function decode($jwt, $key, array $allowed_algs = array()) } // Check the signature if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { - return static::saveErr("Signature verification failed"); + return static::saveErr('Signature verification failed'); } // Check if the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { return static::saveErr( - "Cannot handle token prior to " . date(\DateTime::ISO8601, $payload->nbf) + 'Cannot handle token prior to ' . date(\DateTime::ISO8601, $payload->nbf) ); } // Check that this token has been created before "now". This prevents @@ -111,12 +111,12 @@ public static function decode($jwt, $key, array $allowed_algs = array()) // correctly used the nbf claim). if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { return static::saveErr( - "Cannot handle token prior to " . date(\DateTime::ISO8601, $payload->iat) + 'Cannot handle token prior to ' . date(\DateTime::ISO8601, $payload->iat) ); } // Check if this token has expired. if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { - return static::saveErr("Expired token"); + return static::saveErr('Expired token'); } return $payload; } @@ -180,7 +180,7 @@ public static function sign($msg, $key, $alg = 'HS256') $signature = ''; $success = openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { - throw new \DomainException("OpenSSL unable to sign data"); + throw new \DomainException('OpenSSL unable to sign data'); } else { return $signature; } @@ -355,7 +355,7 @@ private static function safeStrlen($str) return strlen($str); } - protected static function saveErr($err, $key = "token") + protected static function saveErr($err, $key = 'token') { self::$errorsArray[$key] = $err; return null; diff --git a/src/functions.php b/src/functions.php index 597f9c7..a88c508 100644 --- a/src/functions.php +++ b/src/functions.php @@ -3,7 +3,7 @@ if (!function_exists('auth') && class_exists('Leaf\App')) { /** * Return the leaf auth object - * + * * @return Leaf\Auth */ function auth() diff --git a/tests/AuthSessionTest.php b/tests/AuthSessionTest.php deleted file mode 100644 index 54224ef..0000000 --- a/tests/AuthSessionTest.php +++ /dev/null @@ -1,165 +0,0 @@ - 'login-user', 'password' => 'login-pass']); - - $session = new \Leaf\Http\Session(false); - $user = $session->get('AUTH_USER'); - - expect($user['username'])->toBe('login-user'); -}); - -test('login should set session ttl', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig()); - - $timeBeforeLogin = time(); - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $session = new \Leaf\Http\Session(false); - $sessionTtl = $session->get('SESSION_TTL'); - - expect($sessionTtl > $timeBeforeLogin)->toBeTrue(); -}); - -test('login should set regenerate session id', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig()); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $originalSessionId = session_id(); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - expect(session_id())->not()->toBe($originalSessionId); -}); - -test('login should set secure session cookie params', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig()); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $cookieParams = session_get_cookie_params(); - - expect($cookieParams['secure'])->toBeTrue(); - expect($cookieParams['httponly'])->toBeTrue(); - expect($cookieParams['samesite'])->toBe('lax'); -}); - -test('register should set session ttl on login', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig()); - - $timeBeforeLogin = time(); - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $session = new \Leaf\Http\Session(false); - $sessionTtl = $session->get('SESSION_TTL'); - - expect($sessionTtl > $timeBeforeLogin)->toBeTrue(); -}); - -test('Session should expire when fetching user, and then login is possible again', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => 2])); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $user = $auth::user(); - expect($user)->not()->toBeNull(); - expect($user['username'])->toBe('login-user'); - - sleep(1); - expect($auth::user())->not()->toBeNull(); - - sleep(2); - expect($auth::user())->toBeNull(); - - $userAfterReLogin = $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - expect($userAfterReLogin)->not()->toBeNull(); - expect($userAfterReLogin['user']['username'])->toBe('login-user'); -}); - -test('Session should not expire when fetching user if session lifetime is 0', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => 0])); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $user = $auth::user(); - expect($user)->not()->toBeNull(); - expect($user['username'])->toBe('login-user'); - - sleep(2); - expect($auth::user())->not()->toBeNull(); -}); - -test('Session should expire when fetching user id', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => 2])); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - expect($auth::id())->not()->toBeNull(); - - sleep(1); - expect($auth::id())->not()->toBeNull(); - - sleep(2); - expect($auth::id())->toBeNull(); -}); - -test('Session should expire when fetching status', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => 2])); - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - expect($auth::status())->not()->toBeNull(); - - sleep(1); - expect($auth::status())->not()->toBeNull(); - - sleep(2); - expect($auth::status())->toBeFalse(); -}); - -test('Session lifetime should set correct session ttl when string is configured instead of timestamp', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => '1 day'])); - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - expect($auth::status())->not()->toBeNull(); - - $timestampOneDay = 60 * 60 * 24; - $session = new \Leaf\Http\Session(false); - $sessionTtl = $session->get('SESSION_TTL'); - - expect($sessionTtl)->toBe(time() + $timestampOneDay); -}); - -test('Login should throw error when lifetime string is invalid', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => 'invalid string'])); - - expect(fn() => $auth::login(['username' => 'login-user', 'password' => 'login-pass'])) - ->toThrow(Exception::class, 'Provided string could not be converted to time'); -}); diff --git a/tests/AuthTest.php b/tests/AuthTest.php deleted file mode 100644 index 660edb5..0000000 --- a/tests/AuthTest.php +++ /dev/null @@ -1,17 +0,0 @@ - false])); - $response = $auth::register(['username' => 'test-user', 'password' => 'test-password']); - - expect($response['user']['username'])->toBe('test-user'); -}); diff --git a/tests/Pest.php b/tests/Pest.php index 642d828..e3ca842 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,118 +1,90 @@ in('Feature'); - -/* -|-------------------------------------------------------------------------- -| Expectations -|-------------------------------------------------------------------------- -| -| When you're writing tests, you often need to check that values meet certain conditions. The -| "expect()" function gives you access to a set of "expectations" methods that you can use -| to assert different things. Of course, you may extend the Expectation API at any time. -| -*/ - -expect()->extend('toBeOne', function () { - return $this->toBe(1); -}); - -/* -|-------------------------------------------------------------------------- -| Functions -|-------------------------------------------------------------------------- -| -| While Pest is very powerful out-of-the-box, you may have some testing code specific to your -| project that you don't want to repeat in every file. Here you can also expose helpers as -| global functions to help you to reduce the number of lines of code in your test files. -| -*/ - -function createUsersTable() +function createUsersTable($table = 'users', $dynamicId = false) { $db = new \Leaf\Db(); - $db->connect(...getConnectionConfig()); + $db->connect(getConnectionConfig()); + + // $auth = new \Leaf\Auth(); + // $auth->dbConnection($db->connection()); $db->createTableIfNotExists( - 'users', + $table, [ - 'id' => 'int NOT NULL AUTO_INCREMENT', + // using varchar(255) to mimic binary(16) for uuid + 'id' => $dynamicId ? 'varchar(255)' : 'int NOT NULL AUTO_INCREMENT', 'username' => 'varchar(255)', + 'email' => 'varchar(255)', 'password' => 'varchar(255)', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'PRIMARY KEY' => '(id)', ] )->execute(); + + $db->close(); } -function haveRegisteredUser(string $username, string $password): array +function deleteUser(string $username, $table = 'users') { - \Leaf\Auth\Core::connect(...getConnectionConfig('mysql')); - - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['session' => false])); + $db = new \Leaf\Db(); + $db->connect(getConnectionConfig()); - return $auth::register(['username' => $username, 'password' => $password]); + $db->delete($table)->where('username', $username)->execute(); } -function deleteUser(string $username) +function getConnectionConfig(): array { - $db = new \Leaf\Db(); - $db->connect(...getConnectionConfig()); - - $db->delete('users')->where('username', '=', $username)->execute(); + return [ + 'port' => '3306', + 'host' => '127.0.0.1', + 'username' => 'root', + 'password' => '', + 'dbname' => 'atest', + ]; } -function getConnectionConfig(?string $dbType = null): array +function auth(): \Leaf\Auth { - $config = ['localhost', 'leaf', 'root', 'root']; + $db = new \Leaf\Db(); + $db->connect(getConnectionConfig()); - if ($dbType) { - $config[] = $dbType; - } + $auth = new \Leaf\Auth(); + $auth->dbConnection($db->connection()); - return $config; + return $auth; } function getAuthConfig(array $settingsReplacement = []): array { $settings = [ + 'id.key' => 'id', + 'id.uuid' => null, + 'db.table' => 'users', - 'password' => false, - 'timestamps' => false, + + 'timestamps' => true, 'timestamps.format' => 'c', + + 'password' => true, 'password.encode' => null, 'password.verify' => null, 'password.key' => 'password', - 'HIDE_ID' => true, - 'id.key' => 'id', - 'id.uuid' => false, - 'HIDE_PASSWORD' => true, - 'messages.loginParamsError' => 'Incorrect credentials!', - 'messages.loginPasswordError' => 'Password is incorrect!', - 'session' => true, - 'SESSION_ON_REGISTER' => false, - 'GUARD_LOGIN' => '/auth/login', - 'GUARD_REGISTER' => '/auth/register', - 'GUARD_HOME' => '/home', - 'GUARD_LOGOUT' => '/auth/logout', - 'SAVE_SESSION_JWT' => false, + + 'unique' => ['email', 'username'], + 'hidden' => ['field.id', 'field.password'], + + 'session' => false, + 'session.logout' => null, + 'session.register' => null, + 'session.lifetime' => 60 * 60 * 24, + 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], + 'token.lifetime' => null, 'token.secret' => '@_leaf$0Secret!', - 'SESSION_REDIRECT_ON_LOGIN' => false, - 'SESSION_LIFETIME' => 60 * 60 * 24, + + 'messages.loginParamsError' => 'Incorrect credentials!', + 'messages.loginPasswordError' => 'Password is incorrect!', ]; return array_replace($settings, $settingsReplacement); diff --git a/tests/auth.test.php b/tests/auth.test.php new file mode 100644 index 0000000..0ccfcad --- /dev/null +++ b/tests/auth.test.php @@ -0,0 +1,152 @@ +config(['timestamps.format' => 'YYYY-MM-DD HH:MM:ss']); + + $response = $auth->register([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +}); + +test('register should fail if user already exists', function () { + $auth = auth(); + + $auth->config([ + 'timestamps.format' => 'YYYY-MM-DD HH:MM:ss', + 'unique' => ['username', 'email'] + ]); + + $response = $auth->register([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]); + + expect($response)->toBe(false); + expect($auth->errors()['email'])->toBe('email already exists'); + expect($auth->errors()['username'])->toBe('username already exists'); +}); + +test('login should retrieve user from database', function () { + $auth = auth(); + + $response = $auth->login([ + 'username' => 'test-user', + 'password' => 'password' + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +}); + +test('login should fail if user does not exist', function () { + $auth = auth(); + + $response = $auth->login([ + 'username' => 'non-existent-user', + 'password' => 'password' + ]); + + expect($response)->toBe(false); + expect($auth->errors()['auth'])->toBe('Incorrect credentials!'); +}); + +test('login should fail if password is wrong', function () { + $db = new \Leaf\Db(); + $db->connect(getConnectionConfig()); + + $db + ->insert('users') + ->params([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => '$2y$10$91T2Y5/D4e9QXw8EgU33E.9J1N23hHg.6lG5ofVhh69la492kqKga', + ]) + ->execute(); + + $auth = new \Leaf\Auth(); + $auth->dbConnection($db->connection()); + + $userData = $auth->login([ + 'username' => 'test-user', + 'password' => 'wrong-password' + ]); + + expect($userData)->toBe(false); + expect($auth->errors()['password'])->toBe('Password is incorrect!'); +}); + +test('update should update user in database', function () { + $auth = auth(); + + $auth->useSession(); + + $data = $auth->login([ + 'username' => 'test-user', + 'password' => 'password' + ]); + + if (!$data) { + $this->fail(json_encode($auth->errors())); + } + + $response = $auth->update([ + 'username' => 'test-user22', + 'email' => 'test-user22@test.com', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + $auth->config(['session' => false]); + + expect($response['user']['username'])->toBe('test-user22'); + expect($response['user']['email'])->toBe('test-user22@test.com'); +}); + +test('update should fail if user already exists', function () { + $auth = auth(); + + $auth->useSession(); + + $data = $auth->login([ + 'username' => 'test-user22', + 'password' => 'password' + ]); + + if (!$data) { + $this->fail(json_encode($auth->errors())); + } + + $response = $auth->update([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + ]); + + expect($response)->toBe(false); + expect($auth->errors()['email'])->toBe('email already exists'); + expect($auth->errors()['username'])->toBe('username already exists'); +}); diff --git a/tests/extra.test.php b/tests/extra.test.php new file mode 100644 index 0000000..ff33dca --- /dev/null +++ b/tests/extra.test.php @@ -0,0 +1,27 @@ +config(['timestamps.format' => 'YYYY-MM-DD HH:MM:ss']); + + $auth->register([ + 'username' => 'extra-user', + 'email' => 'extra-user@example.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + ]); +}); + +afterAll(function () { + deleteUser('extra-user'); +}); + +test('login should produce valid token', function () { + $auth = auth(); + + $auth->config(['hidden' => ['password']]); + $data = $auth->login(['username' => 'extra-user', 'password' => 'login-pass']); + + expect($auth->validateUserToken($data['token'])->user_id)->toBe((string) $data['user']['id']); +}); diff --git a/tests/session.test.php b/tests/session.test.php new file mode 100644 index 0000000..cd331f6 --- /dev/null +++ b/tests/session.test.php @@ -0,0 +1,183 @@ +config(['timestamps.format' => 'YYYY-MM-DD HH:MM:ss']); + + $auth->register([ + 'username' => 'login-user', + 'email' => 'login-user@example.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + ]); +}); + +afterEach(function () { + if (!session_status()) { + session_start(); + } + + session_destroy(); +}); + +afterAll(function () { + deleteUser('login-user'); +}); + +test('login should set user session', function () { + $auth = auth(); + $auth->useSession(); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $session = new \Leaf\Http\Session(false); + $user = $session->get('auth.token'); + + expect($user['username'])->toBe('login-user'); +}); + +test('login should set session ttl', function () { + $auth = auth(); + $auth->useSession(); + + $timeBeforeLogin = time(); + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $session = new \Leaf\Http\Session(false); + $sessionTtl = $session->get('session.ttl'); + + expect($sessionTtl > $timeBeforeLogin)->toBeTrue(); +}); + +test('login should set regenerate session id', function () { + $auth = auth(); + $auth->useSession(); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $originalSessionId = session_id(); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + expect(session_id())->not()->toBe($originalSessionId); +}); + +test('login should set secure session cookie params', function () { + $auth = auth(); + $auth->useSession(); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $cookieParams = session_get_cookie_params(); + + expect($cookieParams['secure'])->toBeTrue(); + expect($cookieParams['httponly'])->toBeTrue(); + expect($cookieParams['samesite'])->toBe('lax'); +}); + +test('Session should expire when fetching user, and then login is possible again', function () { + $auth = new \Leaf\Auth(); + $auth->config(['session.lifetime' => 2]); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $user = $auth->user(); + + expect($user)->not()->toBeNull(); + expect($user['username'])->toBe('login-user'); + + sleep(1); + expect($auth->user())->not()->toBeNull(); + + sleep(2); + expect($auth->user())->toBeNull(); + + $userAfterReLogin = $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + expect($userAfterReLogin)->not()->toBeNull(); + expect($userAfterReLogin['user']['username'])->toBe('login-user'); +}); + +test('Session should not expire when fetching user if session lifetime is 0', function () { + $auth = new \Leaf\Auth(); + $auth->config(['session.lifetime' => 0]); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $user = $auth->user(); + + expect($user)->not()->toBeNull(); + expect($user['username'])->toBe('login-user'); + + sleep(2); + expect($auth->user())->not()->toBeNull(); +}); + +test('Session should expire when fetching user id', function () { + $auth = new \Leaf\Auth(); + + $auth->config(['session.lifetime' => 2]); + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + expect($auth->id())->not()->toBeNull(); + + sleep(1); + expect($auth->id())->not()->toBeNull(); + + sleep(2); + expect($auth->id())->toBeNull(); +}); + +test('Session should expire when fetching status', function () { + $auth = new \Leaf\Auth(); + + $auth->config(['session.lifetime' => 2]); + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + expect($auth->status())->not()->toBeNull(); + + sleep(1); + expect($auth->status())->not()->toBeNull(); + + sleep(2); + expect($auth->status())->toBeFalse(); +}); + +test('Session lifetime should set correct session ttl when string is configured instead of timestamp', function () { + $auth = new \Leaf\Auth(); + + $auth->config(['session.lifetime' => '1 day']); + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + expect($auth->status())->not()->toBeNull(); + + $timestampOneDay = 60 * 60 * 24; + $session = new \Leaf\Http\Session(false); + $sessionTtl = $session->get('session.ttl'); + + expect($sessionTtl)->toBe(time() + $timestampOneDay); +}); + +test('Login should throw error when lifetime string is invalid', function () { + $auth = new \Leaf\Auth(); + $auth->config(['session.lifetime' => 'invalid string']); + + expect(function () use ($auth) { + return $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + })->toThrow(Exception::class, 'Provided string could not be converted to time'); +}); + +test('Login should set session ttl on login', function () { + $auth = auth(); + $auth->useSession(); + $auth->config(['session.lifetime' => 2]); + + $timeBeforeLogin = time(); + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $session = new \Leaf\Http\Session(false); + $sessionTtl = $session->get('session.ttl'); + + expect($sessionTtl > $timeBeforeLogin)->toBeTrue(); +}); diff --git a/tests/table-actions.test.php b/tests/table-actions.test.php new file mode 100644 index 0000000..a99c7f7 --- /dev/null +++ b/tests/table-actions.test.php @@ -0,0 +1,92 @@ +config(['session' => false, 'db.table' => 'myusers']); + + $response = $auth->register([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +}); + +test('login should work with user defined table', function () { + $auth = auth(); + $auth->config(['session' => false, 'db.table' => 'myusers']); + + $response = $auth->login([ + 'username' => 'test-user', + 'password' => 'password' + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +}); + +test('update should work with user defined table', function () { + $auth = auth(); + $auth->config(['session' => true, 'db.table' => 'myusers', 'session.lifetime' => '1 day']); + + $loginData = $auth->login([ + 'username' => 'test-user', + 'password' => 'password' + ]); + + if (!$loginData) { + $this->fail(json_encode($auth->errors())); + } + + $response = $auth->update([ + 'username' => 'test-user55', + 'email' => 'test-user55@example.com', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user55'); + expect($response['user']['email'])->toBe('test-user55@example.com'); +}); + +test('user table can use uuid as id', function () { + createUsersTable('uuid_users', true); + + $auth = auth(); + $auth->config(['session' => false, 'db.table' => 'uuid_users']); + + $response = $auth->register([ + 'id' => '123e4567-e89b-12d3-a456-426614174000', + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +});