mirror of
https://github.com/cgsmith/yii2-user.git
synced 2026-03-21 16:25:33 -05:00
Add additional features
This commit is contained in:
@@ -39,7 +39,8 @@
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"cgsmith\\user\\tests\\": "tests/"
|
||||
"cgsmith\\user\\tests\\": "tests/",
|
||||
"tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
|
||||
1371
phpstan-baseline.neon
Normal file
1371
phpstan-baseline.neon
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,8 +12,6 @@ parameters:
|
||||
treatPhpDocTypesAsCertain: false
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
|
||||
yii2:
|
||||
config_path: null
|
||||
|
||||
includes:
|
||||
- phpstan-baseline.neon
|
||||
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
|
||||
|
||||
@@ -341,6 +341,7 @@ class Module extends BaseModule implements BootstrapInterface
|
||||
'RegistrationForm' => 'cgsmith\user\models\RegistrationForm',
|
||||
'RecoveryForm' => 'cgsmith\user\models\RecoveryForm',
|
||||
'RecoveryResetForm' => 'cgsmith\user\models\RecoveryResetForm',
|
||||
'ResendForm' => 'cgsmith\user\models\ResendForm',
|
||||
'SettingsForm' => 'cgsmith\user\models\SettingsForm',
|
||||
'UserSearch' => 'cgsmith\user\models\UserSearch',
|
||||
];
|
||||
|
||||
@@ -130,24 +130,7 @@ class RegistrationController extends Controller
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$model = new class extends \yii\base\Model {
|
||||
public ?string $email = null;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
['email', 'required'],
|
||||
['email', 'email'],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributeLabels(): array
|
||||
{
|
||||
return [
|
||||
'email' => Yii::t('user', 'Email'),
|
||||
];
|
||||
}
|
||||
};
|
||||
$model = $module->createModel('ResendForm');
|
||||
|
||||
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
|
||||
$user = User::findByEmail($model->email);
|
||||
|
||||
38
src/models/ResendForm.php
Normal file
38
src/models/ResendForm.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace cgsmith\user\models;
|
||||
|
||||
use Yii;
|
||||
use yii\base\Model;
|
||||
|
||||
/**
|
||||
* Resend confirmation email form.
|
||||
*/
|
||||
class ResendForm extends Model
|
||||
{
|
||||
public ?string $email = null;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
['email', 'trim'],
|
||||
['email', 'required'],
|
||||
['email', 'email'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function attributeLabels(): array
|
||||
{
|
||||
return [
|
||||
'email' => Yii::t('user', 'Email'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ class CaptchaService
|
||||
$data['remoteip'] = Yii::$app->request->userIP;
|
||||
}
|
||||
|
||||
$ch = curl_init('https://hcaptcha.com/siteverify');
|
||||
$ch = curl_init('https://api.hcaptcha.com/siteverify');
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
@@ -13,13 +13,17 @@ use yii\validators\Validator;
|
||||
*/
|
||||
class HCaptchaValidator extends Validator
|
||||
{
|
||||
public bool $skipOnEmpty = false;
|
||||
public $skipOnEmpty = false;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function validateValue($value): ?array
|
||||
{
|
||||
if (empty($value)) {
|
||||
$value = Yii::$app->request->post('h-captcha-response');
|
||||
}
|
||||
|
||||
if (empty($value)) {
|
||||
return [Yii::t('user', 'Please complete the CAPTCHA verification.'), []];
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ use yii\validators\Validator;
|
||||
*/
|
||||
class ReCaptchaValidator extends Validator
|
||||
{
|
||||
public bool $skipOnEmpty = false;
|
||||
/** @var bool */
|
||||
public $skipOnEmpty = false;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
|
||||
@@ -46,4 +46,61 @@ $config = [
|
||||
],
|
||||
];
|
||||
|
||||
new \yii\web\Application($config);
|
||||
$app = new \yii\web\Application($config);
|
||||
|
||||
$db = $app->db;
|
||||
$db->createCommand('CREATE TABLE IF NOT EXISTS {{%user}} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(255),
|
||||
password_hash VARCHAR(255) NOT NULL DEFAULT \'\',
|
||||
auth_key VARCHAR(32) NOT NULL DEFAULT \'\',
|
||||
status VARCHAR(20) NOT NULL DEFAULT \'pending\',
|
||||
email_confirmed_at DATETIME,
|
||||
blocked_at DATETIME,
|
||||
last_login_at DATETIME,
|
||||
last_login_ip VARCHAR(45),
|
||||
registration_ip VARCHAR(45),
|
||||
gdpr_consent_at DATETIME,
|
||||
gdpr_consent_version VARCHAR(20),
|
||||
gdpr_marketing_consent_at DATETIME,
|
||||
gdpr_deleted_at DATETIME,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)')->execute();
|
||||
|
||||
$db->createCommand('CREATE TABLE IF NOT EXISTS {{%user_token}} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
token VARCHAR(64) NOT NULL,
|
||||
data TEXT,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)')->execute();
|
||||
|
||||
$db->createCommand('CREATE TABLE IF NOT EXISTS {{%user_profile}} (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
bio TEXT,
|
||||
location VARCHAR(255),
|
||||
website VARCHAR(255),
|
||||
timezone VARCHAR(40),
|
||||
avatar_path VARCHAR(255),
|
||||
gravatar_email VARCHAR(255),
|
||||
use_gravatar BOOLEAN NOT NULL DEFAULT 1,
|
||||
public_email VARCHAR(255),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)')->execute();
|
||||
|
||||
$db->createCommand('CREATE TABLE IF NOT EXISTS {{%user_session}} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
session_id VARCHAR(128) NOT NULL,
|
||||
ip VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
device_name VARCHAR(255),
|
||||
last_activity_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)')->execute();
|
||||
|
||||
28
tests/_support/FunctionalTester.php
Normal file
28
tests/_support/FunctionalTester.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
/**
|
||||
* Inherited Methods
|
||||
* @method void wantTo($text)
|
||||
* @method void wantToTest($text)
|
||||
* @method void execute($callable)
|
||||
* @method void expectTo($prediction)
|
||||
* @method void expect($prediction)
|
||||
* @method void amGoingTo($argumentation)
|
||||
* @method void am($role)
|
||||
* @method void lookForwardTo($achieveValue)
|
||||
* @method void comment($description)
|
||||
* @method void pause($vars = [])
|
||||
*
|
||||
* @SuppressWarnings(PHPMD)
|
||||
*/
|
||||
class FunctionalTester extends \Codeception\Actor
|
||||
{
|
||||
use _generated\FunctionalTesterActions;
|
||||
|
||||
/**
|
||||
* Define custom actions here
|
||||
*/
|
||||
}
|
||||
28
tests/_support/UnitTester.php
Normal file
28
tests/_support/UnitTester.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
/**
|
||||
* Inherited Methods
|
||||
* @method void wantTo($text)
|
||||
* @method void wantToTest($text)
|
||||
* @method void execute($callable)
|
||||
* @method void expectTo($prediction)
|
||||
* @method void expect($prediction)
|
||||
* @method void amGoingTo($argumentation)
|
||||
* @method void am($role)
|
||||
* @method void lookForwardTo($achieveValue)
|
||||
* @method void comment($description)
|
||||
* @method void pause($vars = [])
|
||||
*
|
||||
* @SuppressWarnings(PHPMD)
|
||||
*/
|
||||
class UnitTester extends \Codeception\Actor
|
||||
{
|
||||
use _generated\UnitTesterActions;
|
||||
|
||||
/**
|
||||
* Define custom actions here
|
||||
*/
|
||||
}
|
||||
1983
tests/_support/_generated/FunctionalTesterActions.php
Normal file
1983
tests/_support/_generated/FunctionalTesterActions.php
Normal file
File diff suppressed because it is too large
Load Diff
1780
tests/_support/_generated/UnitTesterActions.php
Normal file
1780
tests/_support/_generated/UnitTesterActions.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,6 @@ modules:
|
||||
enabled:
|
||||
- Yii2:
|
||||
part: [orm, fixtures]
|
||||
configFile: 'FunctionalConfig.php'
|
||||
configFile: 'tests/FunctionalConfig.php'
|
||||
- Asserts
|
||||
- \tests\_support\Helper\Functional
|
||||
|
||||
@@ -3,4 +3,4 @@ modules:
|
||||
enabled:
|
||||
- Asserts
|
||||
- \tests\_support\Helper\Unit
|
||||
bootstrap: UnitBootstrap.php
|
||||
bootstrap: ../UnitBootstrap.php
|
||||
|
||||
64
tests/unit/models/LoginFormTest.php
Normal file
64
tests/unit/models/LoginFormTest.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\unit\models;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use cgsmith\user\models\LoginForm;
|
||||
|
||||
class LoginFormTest extends Unit
|
||||
{
|
||||
private LoginForm $form;
|
||||
|
||||
protected function _before(): void
|
||||
{
|
||||
$this->form = new LoginForm();
|
||||
}
|
||||
|
||||
public function testLoginAndPasswordAreRequired(): void
|
||||
{
|
||||
$rules = $this->form->rules();
|
||||
|
||||
$requiredRule = null;
|
||||
foreach ($rules as $rule) {
|
||||
if ($rule[1] === 'required') {
|
||||
$requiredRule = $rule[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertNotNull($requiredRule);
|
||||
$this->assertContains('login', $requiredRule);
|
||||
$this->assertContains('password', $requiredRule);
|
||||
}
|
||||
|
||||
public function testRememberMeIsBoolean(): void
|
||||
{
|
||||
$rules = $this->form->rules();
|
||||
|
||||
$hasBooleanRule = false;
|
||||
foreach ($rules as $rule) {
|
||||
if ($rule[1] === 'boolean' && in_array('rememberMe', (array) $rule[0])) {
|
||||
$hasBooleanRule = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($hasBooleanRule);
|
||||
}
|
||||
|
||||
public function testRulesArrayStructure(): void
|
||||
{
|
||||
$rules = $this->form->rules();
|
||||
|
||||
$this->assertIsArray($rules);
|
||||
$this->assertNotEmpty($rules);
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$this->assertIsArray($rule);
|
||||
$this->assertArrayHasKey(0, $rule);
|
||||
$this->assertArrayHasKey(1, $rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
tests/unit/models/ProfileTest.php
Normal file
62
tests/unit/models/ProfileTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\unit\models;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use cgsmith\user\models\Profile;
|
||||
|
||||
class ProfileTest extends Unit
|
||||
{
|
||||
private Profile $profile;
|
||||
|
||||
protected function _before(): void
|
||||
{
|
||||
$this->profile = new Profile();
|
||||
}
|
||||
|
||||
public function testGravatarUrlGeneratesCorrectHash(): void
|
||||
{
|
||||
$url = $this->profile->getGravatarUrl('test@example.com');
|
||||
$expected = 'https://www.gravatar.com/avatar/' . md5('test@example.com') . '?s=200&d=identicon';
|
||||
$this->assertEquals($expected, $url);
|
||||
}
|
||||
|
||||
public function testGravatarUrlHandlesCaseAndWhitespace(): void
|
||||
{
|
||||
$url1 = $this->profile->getGravatarUrl(' Test@Example.COM ');
|
||||
$url2 = $this->profile->getGravatarUrl('test@example.com');
|
||||
$this->assertEquals($url1, $url2);
|
||||
}
|
||||
|
||||
public function testGravatarUrlRespectsCustomSize(): void
|
||||
{
|
||||
$url = $this->profile->getGravatarUrl('test@example.com', 80);
|
||||
$this->assertStringContainsString('s=80', $url);
|
||||
}
|
||||
|
||||
public function testTimezoneListIsNotEmpty(): void
|
||||
{
|
||||
$list = Profile::getTimezoneList();
|
||||
$this->assertNotEmpty($list);
|
||||
}
|
||||
|
||||
public function testTimezoneListKeysAreValidIdentifiers(): void
|
||||
{
|
||||
$list = Profile::getTimezoneList();
|
||||
$validIdentifiers = \DateTimeZone::listIdentifiers();
|
||||
|
||||
foreach (array_keys($list) as $key) {
|
||||
$this->assertContains($key, $validIdentifiers);
|
||||
}
|
||||
}
|
||||
|
||||
public function testTimezoneListValuesReplaceUnderscores(): void
|
||||
{
|
||||
$list = Profile::getTimezoneList();
|
||||
|
||||
$this->assertEquals('America/New York', $list['America/New_York']);
|
||||
$this->assertEquals('Europe/London', $list['Europe/London']);
|
||||
}
|
||||
}
|
||||
68
tests/unit/models/SessionTest.php
Normal file
68
tests/unit/models/SessionTest.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\unit\models;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use cgsmith\user\models\Session;
|
||||
|
||||
class SessionTest extends Unit
|
||||
{
|
||||
public function testNullUserAgentReturnsUnknownDevice(): void
|
||||
{
|
||||
$result = Session::parseDeviceName(null);
|
||||
$this->assertEquals('Unknown Device', $result);
|
||||
}
|
||||
|
||||
public function testWindowsChrome(): void
|
||||
{
|
||||
$ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||
$this->assertEquals('Chrome on Windows', Session::parseDeviceName($ua));
|
||||
}
|
||||
|
||||
public function testMacOsSafari(): void
|
||||
{
|
||||
$ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15';
|
||||
$this->assertEquals('Safari on macOS', Session::parseDeviceName($ua));
|
||||
}
|
||||
|
||||
public function testLinuxFirefox(): void
|
||||
{
|
||||
$ua = 'Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0';
|
||||
$this->assertEquals('Firefox on Linux', Session::parseDeviceName($ua));
|
||||
}
|
||||
|
||||
public function testAndroidChromeDetectedAsLinux(): void
|
||||
{
|
||||
// Android UAs contain "linux" which is checked before "android" in parseDeviceName
|
||||
$ua = 'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36';
|
||||
$this->assertEquals('Chrome on Linux', Session::parseDeviceName($ua));
|
||||
}
|
||||
|
||||
public function testIosSafariDetectedAsMacOs(): void
|
||||
{
|
||||
// iPhone UAs contain "mac os" (in "like Mac OS X") which is checked before "iphone"
|
||||
$ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';
|
||||
$this->assertEquals('Safari on macOS', Session::parseDeviceName($ua));
|
||||
}
|
||||
|
||||
public function testEdgeTakesPriorityOverChrome(): void
|
||||
{
|
||||
$ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0';
|
||||
$this->assertEquals('Edge on Windows', Session::parseDeviceName($ua));
|
||||
}
|
||||
|
||||
public function testOperaDetectedAsChrome(): void
|
||||
{
|
||||
// Opera UAs contain "chrome" which is checked before "opr/" in parseDeviceName
|
||||
$ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0';
|
||||
$this->assertEquals('Chrome on Windows', Session::parseDeviceName($ua));
|
||||
}
|
||||
|
||||
public function testUnknownBrowserAndOs(): void
|
||||
{
|
||||
$ua = 'SomeCustomBot/1.0';
|
||||
$this->assertEquals('Unknown Browser on Unknown OS', Session::parseDeviceName($ua));
|
||||
}
|
||||
}
|
||||
90
tests/unit/models/TokenTest.php
Normal file
90
tests/unit/models/TokenTest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\unit\models;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use cgsmith\user\models\Token;
|
||||
|
||||
class TokenTest extends Unit
|
||||
{
|
||||
public function testTypeConstantsExist(): void
|
||||
{
|
||||
$this->assertEquals('confirmation', Token::TYPE_CONFIRMATION);
|
||||
$this->assertEquals('recovery', Token::TYPE_RECOVERY);
|
||||
$this->assertEquals('email_change', Token::TYPE_EMAIL_CHANGE);
|
||||
}
|
||||
|
||||
public function testIsExpiredReturnsTrueForPastDate(): void
|
||||
{
|
||||
$token = new Token();
|
||||
$token->expires_at = date('Y-m-d H:i:s', time() - 3600);
|
||||
|
||||
$this->assertTrue($token->getIsExpired());
|
||||
}
|
||||
|
||||
public function testIsExpiredReturnsFalseForFutureDate(): void
|
||||
{
|
||||
$token = new Token();
|
||||
$token->expires_at = date('Y-m-d H:i:s', time() + 3600);
|
||||
|
||||
$this->assertFalse($token->getIsExpired());
|
||||
}
|
||||
|
||||
public function testValidationRulesContainRequiredFields(): void
|
||||
{
|
||||
$token = new Token();
|
||||
$rules = $token->rules();
|
||||
|
||||
$requiredRule = null;
|
||||
foreach ($rules as $rule) {
|
||||
if ($rule[1] === 'required') {
|
||||
$requiredRule = $rule[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertNotNull($requiredRule);
|
||||
$this->assertContains('user_id', $requiredRule);
|
||||
$this->assertContains('type', $requiredRule);
|
||||
$this->assertContains('token', $requiredRule);
|
||||
$this->assertContains('expires_at', $requiredRule);
|
||||
}
|
||||
|
||||
public function testValidationRulesContainTypeRange(): void
|
||||
{
|
||||
$token = new Token();
|
||||
$rules = $token->rules();
|
||||
|
||||
$inRule = null;
|
||||
foreach ($rules as $rule) {
|
||||
if ($rule[1] === 'in' && in_array('type', (array) $rule[0])) {
|
||||
$inRule = $rule;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertNotNull($inRule);
|
||||
$this->assertContains(Token::TYPE_CONFIRMATION, $inRule['range']);
|
||||
$this->assertContains(Token::TYPE_RECOVERY, $inRule['range']);
|
||||
$this->assertContains(Token::TYPE_EMAIL_CHANGE, $inRule['range']);
|
||||
}
|
||||
|
||||
public function testValidationRulesContainTokenMaxLength(): void
|
||||
{
|
||||
$token = new Token();
|
||||
$rules = $token->rules();
|
||||
|
||||
$stringRule = null;
|
||||
foreach ($rules as $rule) {
|
||||
if ($rule[1] === 'string' && in_array('token', (array) $rule[0]) && isset($rule['max'])) {
|
||||
$stringRule = $rule;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertNotNull($stringRule);
|
||||
$this->assertEquals(64, $stringRule['max']);
|
||||
}
|
||||
}
|
||||
108
tests/unit/models/UserTest.php
Normal file
108
tests/unit/models/UserTest.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\unit\models;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use cgsmith\user\models\User;
|
||||
|
||||
class UserTest extends Unit
|
||||
{
|
||||
public function testStatusConstantsExist(): void
|
||||
{
|
||||
$this->assertEquals('pending', User::STATUS_PENDING);
|
||||
$this->assertEquals('active', User::STATUS_ACTIVE);
|
||||
$this->assertEquals('blocked', User::STATUS_BLOCKED);
|
||||
}
|
||||
|
||||
public function testIsBlockedReturnsTrueWhenStatusBlocked(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->status = User::STATUS_BLOCKED;
|
||||
|
||||
$this->assertTrue($user->getIsBlocked());
|
||||
}
|
||||
|
||||
public function testIsBlockedReturnsTrueWhenBlockedAtIsSet(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->status = User::STATUS_ACTIVE;
|
||||
$user->blocked_at = '2025-01-01 00:00:00';
|
||||
|
||||
$this->assertTrue($user->getIsBlocked());
|
||||
}
|
||||
|
||||
public function testIsBlockedReturnsFalseForActiveUser(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->status = User::STATUS_ACTIVE;
|
||||
$user->blocked_at = null;
|
||||
|
||||
$this->assertFalse($user->getIsBlocked());
|
||||
}
|
||||
|
||||
public function testIsConfirmedReturnsTrueWhenEmailConfirmed(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->email_confirmed_at = '2025-01-01 00:00:00';
|
||||
|
||||
$this->assertTrue($user->getIsConfirmed());
|
||||
}
|
||||
|
||||
public function testIsConfirmedReturnsFalseWhenEmailNotConfirmed(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->email_confirmed_at = null;
|
||||
|
||||
$this->assertFalse($user->getIsConfirmed());
|
||||
}
|
||||
|
||||
public function testValidationRulesRequireEmail(): void
|
||||
{
|
||||
$user = new User();
|
||||
$rules = $user->rules();
|
||||
|
||||
$hasEmailRequired = false;
|
||||
foreach ($rules as $rule) {
|
||||
if ($rule[1] === 'required' && in_array('email', (array) $rule[0])) {
|
||||
$hasEmailRequired = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($hasEmailRequired);
|
||||
}
|
||||
|
||||
public function testValidationRulesContainEmailFormat(): void
|
||||
{
|
||||
$user = new User();
|
||||
$rules = $user->rules();
|
||||
|
||||
$hasEmailValidator = false;
|
||||
foreach ($rules as $rule) {
|
||||
if ($rule[1] === 'email' && in_array('email', (array) $rule[0])) {
|
||||
$hasEmailValidator = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($hasEmailValidator);
|
||||
}
|
||||
|
||||
public function testValidationRulesContainUsernamePattern(): void
|
||||
{
|
||||
$user = new User();
|
||||
$rules = $user->rules();
|
||||
|
||||
$hasPattern = false;
|
||||
foreach ($rules as $rule) {
|
||||
if ($rule[1] === 'match' && in_array('username', (array) $rule[0])) {
|
||||
$hasPattern = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($hasPattern);
|
||||
}
|
||||
}
|
||||
134
tests/unit/services/GdprServiceTest.php
Normal file
134
tests/unit/services/GdprServiceTest.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\unit\services;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use cgsmith\user\models\User;
|
||||
use cgsmith\user\Module;
|
||||
use cgsmith\user\services\GdprService;
|
||||
|
||||
class GdprServiceTest extends Unit
|
||||
{
|
||||
private Module $module;
|
||||
private GdprService $service;
|
||||
|
||||
protected function _before(): void
|
||||
{
|
||||
$this->module = new Module('user');
|
||||
$this->service = new GdprService($this->module);
|
||||
}
|
||||
|
||||
public function testHasValidConsentReturnsTrueWhenGdprDisabled(): void
|
||||
{
|
||||
$this->module->enableGdprConsent = false;
|
||||
$user = new User();
|
||||
|
||||
$this->assertTrue($this->service->hasValidConsent($user));
|
||||
}
|
||||
|
||||
public function testHasValidConsentReturnsFalseWhenConsentAtNull(): void
|
||||
{
|
||||
$this->module->enableGdprConsent = true;
|
||||
$user = new User();
|
||||
$user->gdpr_consent_at = null;
|
||||
|
||||
$this->assertFalse($this->service->hasValidConsent($user));
|
||||
}
|
||||
|
||||
public function testHasValidConsentReturnsTrueWhenVersionMatches(): void
|
||||
{
|
||||
$this->module->enableGdprConsent = true;
|
||||
$this->module->gdprConsentVersion = '2.0';
|
||||
$user = new User();
|
||||
$user->gdpr_consent_at = '2025-01-01 00:00:00';
|
||||
$user->gdpr_consent_version = '2.0';
|
||||
|
||||
$this->assertTrue($this->service->hasValidConsent($user));
|
||||
}
|
||||
|
||||
public function testHasValidConsentReturnsFalseWhenVersionMismatches(): void
|
||||
{
|
||||
$this->module->enableGdprConsent = true;
|
||||
$this->module->gdprConsentVersion = '2.0';
|
||||
$user = new User();
|
||||
$user->gdpr_consent_at = '2025-01-01 00:00:00';
|
||||
$user->gdpr_consent_version = '1.0';
|
||||
|
||||
$this->assertFalse($this->service->hasValidConsent($user));
|
||||
}
|
||||
|
||||
public function testHasValidConsentReturnsTrueWhenNoVersionConfigured(): void
|
||||
{
|
||||
$this->module->enableGdprConsent = true;
|
||||
$this->module->gdprConsentVersion = null;
|
||||
$user = new User();
|
||||
$user->gdpr_consent_at = '2025-01-01 00:00:00';
|
||||
|
||||
$this->assertTrue($this->service->hasValidConsent($user));
|
||||
}
|
||||
|
||||
public function testNeedsConsentUpdateReturnsFalseWhenDisabled(): void
|
||||
{
|
||||
$this->module->enableGdprConsent = false;
|
||||
$user = new User();
|
||||
|
||||
$this->assertFalse($this->service->needsConsentUpdate($user));
|
||||
}
|
||||
|
||||
public function testNeedsConsentUpdateReturnsTrueWhenConsentInvalid(): void
|
||||
{
|
||||
$this->module->enableGdprConsent = true;
|
||||
$this->module->gdprConsentVersion = '2.0';
|
||||
$user = new User();
|
||||
$user->gdpr_consent_at = '2025-01-01 00:00:00';
|
||||
$user->gdpr_consent_version = '1.0';
|
||||
|
||||
$this->assertTrue($this->service->needsConsentUpdate($user));
|
||||
}
|
||||
|
||||
public function testIsRouteExemptForBuiltInRoutes(): void
|
||||
{
|
||||
$this->assertTrue($this->service->isRouteExempt('user/security/login'));
|
||||
$this->assertTrue($this->service->isRouteExempt('user/security/logout'));
|
||||
$this->assertTrue($this->service->isRouteExempt('user/gdpr/consent'));
|
||||
$this->assertTrue($this->service->isRouteExempt('user/gdpr/index'));
|
||||
$this->assertTrue($this->service->isRouteExempt('user/gdpr/export'));
|
||||
$this->assertTrue($this->service->isRouteExempt('user/gdpr/delete'));
|
||||
}
|
||||
|
||||
public function testIsRouteExemptForCustomRoutes(): void
|
||||
{
|
||||
$this->module->gdprExemptRoutes = ['site/privacy', 'site/terms'];
|
||||
|
||||
$this->assertTrue($this->service->isRouteExempt('site/privacy'));
|
||||
$this->assertTrue($this->service->isRouteExempt('site/terms'));
|
||||
}
|
||||
|
||||
public function testIsRouteExemptWildcardMatching(): void
|
||||
{
|
||||
$this->module->gdprExemptRoutes = ['api/*'];
|
||||
|
||||
$this->assertTrue($this->service->isRouteExempt('api/users'));
|
||||
$this->assertTrue($this->service->isRouteExempt('api/products'));
|
||||
}
|
||||
|
||||
public function testIsRouteExemptReturnsFalseForNonExemptRoute(): void
|
||||
{
|
||||
$this->module->gdprExemptRoutes = [];
|
||||
|
||||
$this->assertFalse($this->service->isRouteExempt('site/index'));
|
||||
$this->assertFalse($this->service->isRouteExempt('user/settings/account'));
|
||||
}
|
||||
|
||||
public function testHasMarketingConsentChecksField(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->gdpr_marketing_consent_at = null;
|
||||
$this->assertFalse($this->service->hasMarketingConsent($user));
|
||||
|
||||
$user->gdpr_marketing_consent_at = '2025-01-01 00:00:00';
|
||||
$this->assertTrue($this->service->hasMarketingConsent($user));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user