Skip to content

Commit

Permalink
Merge pull request #16 from crosa7/create-auth-tests-structure
Browse files Browse the repository at this point in the history
Added tests infrastructure, session lifetime and cookie params config
  • Loading branch information
mychidarko authored Aug 17, 2023
2 parents 0f3cbd7 + dec672a commit 6490e56
Show file tree
Hide file tree
Showing 11 changed files with 411 additions and 22 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Run Tests

on: ['push', 'pull_request']

env:
MYSQL_DATABASE: leaf
DB_USER: root
DB_PASSWORD: root

jobs:
ci:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
php: ['7.4', '8.0', '8.1', '8.2']

name: PHP ${{ matrix.php }} - ${{ matrix.os }}

steps:
- name: Checkout
uses: actions/checkout@v2

- name: Initialize MySQL
run: sudo systemctl start mysql.service

- name: Initialize first database
run: |
mysql -e 'CREATE DATABASE ${{ env.MYSQL_DATABASE }};' \
-u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
coverage: xdebug

- name: Install PHP dependencies
run: composer update --no-interaction --no-progress

- name: All Tests
run: composer run-script test
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ composer.lock
package-lock.json
vendor/
test/
phpunit.xml
*.tests.php
workflows
!.github/workflows

# OS Generated
.DS_Store*
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,8 @@
},
"require-dev": {
"leafs/alchemy": "^1.0"
},
"scripts": {
"test": "vendor/bin/pest --colors=always --coverage"
}
}
18 changes: 18 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
<directory suffix=".php">./src</directory>
</include>
</coverage>
</phpunit>
80 changes: 66 additions & 14 deletions src/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,11 @@ public static function login(array $credentials)
$user[static::$settings['ID_KEY']] = $userId;
}

static::$session->set('AUTH_USER', $user);
static::$session->set('HAS_SESSION', true);
self::setUserToSession($user, $token);

if (static::config('SAVE_SESSION_JWT')) {
static::$session->set('AUTH_TOKEN', $token);
if (static::config('SESSION_REDIRECT_ON_LOGIN')) {
exit(header('location: ' . static::config('GUARD_HOME')));
}

exit(header('location: ' . static::config('GUARD_HOME')));
}

$response['user'] = $user;
Expand Down Expand Up @@ -201,12 +198,7 @@ public static function register(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);
}
self::setUserToSession($user, $token);

exit(header('location: ' . static::config('GUARD_HOME')));
} else {
Expand Down Expand Up @@ -375,7 +367,7 @@ public function validate(array $rules): bool
public static function useSession()
{
static::config('USE_SESSION', true);
static::$session = Auth\Session::init();
static::$session = Auth\Session::init(static::config('SESSION_COOKIE_PARAMS'));
}

/**
Expand Down Expand Up @@ -418,6 +410,8 @@ public static function status()
{
static::sessionCheck();

static::expireSession();

return static::$session->get('AUTH_USER') ?? false;
}

Expand All @@ -429,6 +423,10 @@ public static function id()
static::leafDbConnect();

if (static::config('USE_SESSION')) {
if (static::expireSession()) {
return null;
}

return static::$session->get('AUTH_USER')[static::$settings['ID_KEY']] ?? null;
}

Expand Down Expand Up @@ -485,6 +483,31 @@ public static function logout(?string $location = null)
}
}

/**
* @return bool
*/
private static function expireSession(): bool
{
$sessionTtl = static::$session->get('SESSION_TTL');

if (!$sessionTtl) {
return false;
}

$isSessionExpired = time() > $sessionTtl;

if ($isSessionExpired) {
static::$session->unset('AUTH_USER');
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');
}

return $isSessionExpired;
}

/**
* Session last active
*/
Expand All @@ -508,7 +531,7 @@ public static function refresh(bool $clearData = true)

static::$session->set('SESSION_STARTED_AT', time());
static::$session->set('SESSION_LAST_ACTIVITY', time());
static::$session->set('AUTH_SESISON', true);
static::setSessionTtl();

return $success;
}
Expand Down Expand Up @@ -537,4 +560,33 @@ public static function length()

return time() - static::$session->get('SESSION_STARTED_AT');
}

/**
* @param array $user
* @param string $token
*
* @return void
*/
private static function setUserToSession(array $user, string $token): void
{
session_regenerate_id();

static::$session->set('AUTH_USER', $user);
static::$session->set('HAS_SESSION', true);
static::setSessionTtl();

if (static::config('SAVE_SESSION_JWT')) {
static::$session->set('AUTH_TOKEN', $token);
}
}

/**
* @return void
*/
private static function setSessionTtl(): void
{
if ((int)static::config('SESSION_LIFETIME') > 0) {
static::$session->set('SESSION_TTL', time() + (int)static::config('SESSION_LIFETIME'));
}
}
}
5 changes: 5 additions & 0 deletions src/Auth/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
class Core
{
private const TIMESTAMP_OF_ONE_DAY = 60 * 60 * 24;

/**
* All errors caught
*/
Expand Down Expand Up @@ -46,6 +48,9 @@ class Core
'SAVE_SESSION_JWT' => false,
'TOKEN_LIFETIME' => null,
'TOKEN_SECRET' => '@_leaf$0Secret!',
'SESSION_REDIRECT_ON_LOGIN' => true,
'SESSION_LIFETIME' => self::TIMESTAMP_OF_ONE_DAY,
'SESSION_COOKIE_PARAMS' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'],
];

/**
Expand Down
3 changes: 2 additions & 1 deletion src/Auth/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ class Session
*/
protected static $session;

public static function init()
public static function init(array $sessionCookieParams = [])
{
static::$session = new \Leaf\Http\Session(false);

if (!isset($_SESSION)) {
session_set_cookie_params($sessionCookieParams);
session_start();
};

Expand Down
137 changes: 137 additions & 0 deletions tests/AuthSessionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

beforeEach(function () {
createUsersTable();
haveRegisteredUser('login-user', 'login-pass');
});

test('login should set user session', function () {
$auth = new \Leaf\Auth();
$auth::config(getAuthConfig());
$auth::login(['username' => '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();
});

afterEach(function () {
deleteUser('login-user');
});
Loading

0 comments on commit 6490e56

Please sign in to comment.