Parcourir la source

Integration tests #5694

Aleksandr Trofimov il y a 1 mois
Parent
commit
fae693a793

+ 1 - 0
.gitignore

@@ -33,6 +33,7 @@ build.properties
 composer.phar
 tests/vendor
 tests/composer.phar
+tests/.env
 tests/.phpunit.cache
 tests/.phpunit.result.cache
 samples/img/*

+ 20 - 0
build.xml

@@ -155,6 +155,26 @@
         </exec>
     </target>
 
+    <!-- ============================================  -->
+    <!-- Target: test-units                            -->
+    <!-- ============================================  -->
+    <target name="test-units">
+        <echo msg="Testing units..." />
+        <exec executable="${php}" dir="${project.basedir}" checkreturn="true">
+            <arg line="plugins/bin/phpunit -c tests/phpunit.xml --testsuite Units" />
+        </exec>
+    </target>
+
+    <!-- ============================================  -->
+    <!-- Target: test-integration                      -->
+    <!-- ============================================  -->
+    <target name="test-integration">
+        <echo msg="Testing integration..." />
+        <exec executable="${php}" dir="${project.basedir}" checkreturn="true">
+            <arg line="plugins/bin/phpunit -c tests/phpunit.xml --testsuite Integration" />
+        </exec>
+    </target>
+
     <!-- ============================================  -->
     <!-- Target: package                               -->
     <!-- ============================================  -->

+ 2 - 1
composer.json

@@ -113,7 +113,8 @@
     },
     "require-dev": {
         "phpunit/phpunit": "12.*",
-        "phpmd/phpmd": "@stable"
+        "phpmd/phpmd": "@stable",
+        "vlucas/phpdotenv": "^5.6"
     },
     "scripts": {
         "post-update-cmd": [

+ 223 - 2
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "5c0c64d5169a971ca1c5b588ae6a2a31",
+    "content-hash": "b5b527f6989da4ce4e3c899689393a51",
     "packages": [
         {
             "name": "achingbrain/php5-akismet",
@@ -4650,6 +4650,68 @@
             ],
             "time": "2024-05-06T16:37:16+00:00"
         },
+        {
+            "name": "graham-campbell/result-type",
+            "version": "v1.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/GrahamCampbell/Result-Type.git",
+                "reference": "adccca3324eece92ca35463648c12b9e6293c05b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/adccca3324eece92ca35463648c12b9e6293c05b",
+                "reference": "adccca3324eece92ca35463648c12b9e6293c05b",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0",
+                "phpoption/phpoption": "^1.10"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5.52 || ^9.6.34 || ^10.5.63 || ^11.5.55 || ^12.5.14"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "GrahamCampbell\\ResultType\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                }
+            ],
+            "description": "An Implementation Of The Result Type",
+            "keywords": [
+                "Graham Campbell",
+                "GrahamCampbell",
+                "Result Type",
+                "Result-Type",
+                "result"
+            ],
+            "support": {
+                "issues": "https://github.com/GrahamCampbell/Result-Type/issues",
+                "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.2.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-08-24T09:06:52+00:00"
+        },
         {
             "name": "myclabs/deep-copy",
             "version": "1.14.0",
@@ -5031,6 +5093,81 @@
             ],
             "time": "2023-12-11T08:22:20+00:00"
         },
+        {
+            "name": "phpoption/phpoption",
+            "version": "1.10.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/schmittjoh/php-option.git",
+                "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/67b192b6a42ec03944b972d6e633ddec78ad2c6d",
+                "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "phpunit/phpunit": "^8.5.54 || ^9.6.36 || ^10.5.64 || ^11.5.56 || ^12.5.33"
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                },
+                "branch-alias": {
+                    "dev-master": "1.9-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PhpOption\\": "src/PhpOption/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Johannes M. Schmitt",
+                    "email": "schmittjoh@gmail.com",
+                    "homepage": "https://github.com/schmittjoh"
+                },
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                }
+            ],
+            "description": "Option Type for PHP",
+            "keywords": [
+                "language",
+                "option",
+                "php",
+                "type"
+            ],
+            "support": {
+                "issues": "https://github.com/schmittjoh/php-option/issues",
+                "source": "https://github.com/schmittjoh/php-option/tree/1.10.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-08-24T00:54:40+00:00"
+        },
         {
             "name": "phpunit/php-code-coverage",
             "version": "12.5.7",
@@ -6872,6 +7009,90 @@
                 }
             ],
             "time": "2025-12-08T11:19:18+00:00"
+        },
+        {
+            "name": "vlucas/phpdotenv",
+            "version": "v5.7.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/vlucas/phpdotenv.git",
+                "reference": "301c07936b16d88628b126b01d082ba153cf4c40"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/301c07936b16d88628b126b01d082ba153cf4c40",
+                "reference": "301c07936b16d88628b126b01d082ba153cf4c40",
+                "shasum": ""
+            },
+            "require": {
+                "ext-pcre": "*",
+                "graham-campbell/result-type": "^1.2",
+                "php": "^7.2.5 || ^8.0",
+                "phpoption/phpoption": "^1.10",
+                "symfony/polyfill-ctype": "^1.26",
+                "symfony/polyfill-mbstring": "^1.26",
+                "symfony/polyfill-php80": "^1.26"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "ext-filter": "*",
+                "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
+            },
+            "suggest": {
+                "ext-filter": "Required to use the boolean validator."
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                },
+                "branch-alias": {
+                    "dev-master": "5.6-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Dotenv\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Vance Lucas",
+                    "email": "vance@vancelucas.com",
+                    "homepage": "https://github.com/vlucas"
+                }
+            ],
+            "description": "Loads environment variables from `.env` to `$_ENV` and `$_SERVER` automagically, and optionally to `getenv()`.",
+            "keywords": [
+                "dotenv",
+                "env",
+                "environment"
+            ],
+            "support": {
+                "issues": "https://github.com/vlucas/phpdotenv/issues",
+                "source": "https://github.com/vlucas/phpdotenv/tree/v5.7.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-08-24T18:07:49+00:00"
         }
     ],
     "aliases": [],
@@ -6890,5 +7111,5 @@
         "php": ">=8.1"
     },
     "platform-dev": {},
-    "plugin-api-version": "2.6.0"
+    "plugin-api-version": "2.9.0"
 }

+ 7 - 0
tests/.env.example

@@ -0,0 +1,7 @@
+# Copy to tests/.env to override. tests/.env is gitignored.
+# Admin matches install/cmd.php defaults (php cmd.php).
+# Regular user is not created by the installer; create it on the site or skip those cases.
+UNA_TEST_ADMIN_EMAIL=admin@example.com
+UNA_TEST_ADMIN_PASSWORD=unauna
+UNA_TEST_USER_EMAIL=user@example.com
+UNA_TEST_USER_PASSWORD=unauna

+ 10 - 0
tests/README.md

@@ -49,3 +49,13 @@ Alternatively:
 ```
 phing test
 ```
+Unit tests only:
+```
+phing test-units
+```
+Integration tests only:
+```
+phing test-integration
+```
+
+Integration tests read `tests/.env` when present (`cp tests/.env.example tests/.env`). Without that file they use installer defaults. Tests skip an account that is not installed.

+ 8 - 0
tests/bootstrap.php

@@ -6,6 +6,13 @@ use PHPUnit\Framework\Attributes\Before;
 define('BX_SKIP_INSTALL_CHECK', 1);
 
 $aPathInfo = pathinfo(__FILE__);
+$sAutoloadPath = $aPathInfo['dirname'] . '/../plugins/autoload.php';
+if (is_readable($sAutoloadPath)) {
+    require_once($sAutoloadPath);
+    if (class_exists(Dotenv\Dotenv::class))
+        Dotenv\Dotenv::createImmutable($aPathInfo['dirname'])->safeLoad();
+}
+
 $sHeaderPath = $aPathInfo['dirname'] . '/../inc/header.inc.php';
 if (!file_exists($sHeaderPath))
     die("Script is not installed\n");
@@ -170,3 +177,4 @@ class BxDolTestCase extends \PHPUnit\Framework\TestCase
 
 require_once dirname(__FILE__) . '/units/modules/boonex/posts/BxPostsTestCase.php';
 require_once dirname(__FILE__) . '/units/modules/boonex/persons/BxPersonsTestCase.php';
+require_once dirname(__FILE__) . '/integration/BxDolIntegrationTestCase.php';

+ 172 - 0
tests/integration/BxDolIntegrationTestCase.php

@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * Shared helpers for integration tests that talk to a live UNA install.
+ */
+abstract class BxDolIntegrationTestCase extends BxDolTestCase
+{
+    protected $_aAccountAuthBackups;
+    protected $_aLoggedBackup;
+    protected $_aPostBackup;
+    protected $_aCookieBackup;
+
+    protected function setUp(): void
+    {
+        $this->_aAccountAuthBackups = [];
+        foreach (['admin', 'user'] as $sKind) {
+            $oAccount = BxDolAccount::getInstance($this->bxTestEmail($sKind));
+            if (!$oAccount)
+                continue;
+
+            $aInfo = $oAccount->getInfo();
+            $this->_aAccountAuthBackups[(int)$oAccount->id()] = [
+                'login_attempts' => (int)$aInfo['login_attempts'],
+                'locked' => (int)$aInfo['locked'],
+            ];
+        }
+
+        $this->_aLoggedBackup = [
+            'member' => !empty($GLOBALS['logged']['member']),
+            'admin' => !empty($GLOBALS['logged']['admin']),
+        ];
+        $this->_aPostBackup = $_POST;
+        $this->_aCookieBackup = $_COOKIE;
+    }
+
+    protected function tearDown(): void
+    {
+        $this->bxBecomeGuest();
+        $this->bxRestoreAccountAuth();
+
+        $_POST = $this->_aPostBackup ?? [];
+        $_COOKIE = $this->_aCookieBackup ?? [];
+
+        BxDolForm::unSetObjectInstance('sys_login', 'sys_login');
+
+        $GLOBALS['logged']['member'] = $this->_aLoggedBackup['member'] ?? false;
+        $GLOBALS['logged']['admin'] = $this->_aLoggedBackup['admin'] ?? false;
+    }
+
+    protected function bxAccount(string $sKind = 'admin'): BxDolAccount
+    {
+        $sEmail = $this->bxTestEmail($sKind);
+        $oAccount = BxDolAccount::getInstance($sEmail, true);
+        if (!$oAccount)
+            $this->markTestSkipped('Account ' . $sEmail . ' is not installed.');
+
+        return $oAccount;
+    }
+
+    protected function bxBecomeGuest(): void
+    {
+        $oSession = BxDolSession::getInstance();
+        if ($oSession->getId() || $oSession->getUserId() || isLogged())
+            bx_logout(false);
+
+        unset($_COOKIE['memberID'], $_COOKIE['memberPassword'], $_COOKIE[BX_DOL_SESSION_COOKIE]);
+        $this->bxSetLoggedFlags(0);
+    }
+
+    protected function bxSetLoggedFlags(int $iRole): void
+    {
+        $GLOBALS['logged']['member'] = (bool)($iRole & BX_DOL_ROLE_MEMBER);
+        $GLOBALS['logged']['admin'] = (bool)($iRole & BX_DOL_ROLE_ADMIN);
+    }
+
+    protected function bxSyncSessionCookie(): void
+    {
+        $sId = BxDolSession::getInstance()->getId();
+        if ($sId)
+            $_COOKIE[BX_DOL_SESSION_COOKIE] = $sId;
+        else
+            unset($_COOKIE[BX_DOL_SESSION_COOKIE]);
+    }
+
+    protected function bxLoginAs(string $sKind = 'admin', bool $bRememberMe = false): array
+    {
+        $oAccount = $this->bxAccount($sKind);
+        $aInfo = bx_login($oAccount->id(), $bRememberMe);
+        $this->assertIsArray($aInfo, 'bx_login should return account info.');
+        $this->assertSame((int)$oAccount->id(), (int)BxDolSession::getInstance()->getUserId());
+
+        $this->bxSyncSessionCookie();
+        $this->bxSetLoggedFlags((int)$aInfo['role']);
+
+        return $aInfo;
+    }
+
+    protected function bxLoginAsAdmin(bool $bRememberMe = false): array
+    {
+        return $this->bxLoginAs('admin', $bRememberMe);
+    }
+
+    protected function bxLoginAsUser(bool $bRememberMe = false): array
+    {
+        return $this->bxLoginAs('user', $bRememberMe);
+    }
+
+    protected function bxSubmitLoginForm(string $sEmail, string $sPassword): BxBaseFormLogin
+    {
+        BxDolForm::unSetObjectInstance('sys_login', 'sys_login');
+
+        $_POST = [
+            'ID' => $sEmail,
+            'Password' => $sPassword,
+            'role' => (string)BX_DOL_ROLE_MEMBER,
+            'login' => '1',
+        ];
+
+        $oForm = BxDolForm::getObjectInstance('sys_login', 'sys_login');
+        $this->assertInstanceOf(BxBaseFormLogin::class, $oForm);
+
+        $oForm->aParams['csrf']['disable'] = true;
+        $oForm->initChecker();
+
+        return $oForm;
+    }
+
+    protected function bxTestEmail(string $sKind = 'admin'): string
+    {
+        $this->bxAssertAccountKind($sKind);
+        return $sKind === 'user'
+            ? $this->bxEnv('UNA_TEST_USER_EMAIL', 'user@example.com')
+            : $this->bxEnv('UNA_TEST_ADMIN_EMAIL', 'admin@example.com');
+    }
+
+    protected function bxTestPassword(string $sKind = 'admin'): string
+    {
+        $this->bxAssertAccountKind($sKind);
+        return $sKind === 'user'
+            ? $this->bxEnv('UNA_TEST_USER_PASSWORD', 'unauna')
+            : $this->bxEnv('UNA_TEST_ADMIN_PASSWORD', 'unauna');
+    }
+
+    protected function bxAssertAccountKind(string $sKind): void
+    {
+        if ($sKind !== 'admin' && $sKind !== 'user')
+            $this->fail('Unknown test account kind: ' . $sKind);
+    }
+
+    protected function bxEnv(string $sName, string $sDefault): string
+    {
+        $s = getenv($sName);
+        if ($s === false || $s === '')
+            $s = $_ENV[$sName] ?? $_SERVER[$sName] ?? '';
+        return ($s !== '') ? (string)$s : $sDefault;
+    }
+
+    protected function bxRestoreAccountAuth(): void
+    {
+        if (!$this->_aAccountAuthBackups)
+            return;
+
+        foreach ($this->_aAccountAuthBackups as $iId => $aBackup) {
+            BxDolDb::getInstance()->query("UPDATE `sys_accounts` SET `login_attempts` = :attempts, `locked` = :locked WHERE `id` = :id", [
+                'attempts' => $aBackup['login_attempts'],
+                'locked' => $aBackup['locked'],
+                'id' => $iId,
+            ]);
+            BxDolAccount::getInstance($iId, true);
+        }
+    }
+}

+ 124 - 0
tests/integration/login/LoginLogoutTest.php

@@ -0,0 +1,124 @@
+<?php
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Login and logout against a live install using admin and regular-user accounts.
+ */
+#[Group('integration')]
+class LoginLogoutTest extends BxDolIntegrationTestCase
+{
+    static public function providerForAccounts()
+    {
+        return [
+            'admin' => ['admin'],
+            'user' => ['user'],
+        ];
+    }
+
+    #[DataProvider('providerForAccounts')]
+    public function testValidCredentialsAreAccepted(string $sKind): void
+    {
+        $this->bxAccount($sKind);
+        $this->assertSame('', bx_check_password($this->bxTestEmail($sKind), $this->bxTestPassword($sKind), BX_DOL_ROLE_MEMBER));
+    }
+
+    #[DataProvider('providerForAccounts')]
+    public function testInvalidPasswordIsRejected(string $sKind): void
+    {
+        $this->bxAccount($sKind);
+        $sError = bx_check_password($this->bxTestEmail($sKind), 'not-the-password', BX_DOL_ROLE_MEMBER);
+
+        $this->assertNotSame('', $sError);
+        $this->assertFalse(isLogged());
+        $this->assertSame(0, (int)getLoggedId());
+    }
+
+    #[DataProvider('providerForAccounts')]
+    public function testLoginEstablishesSession(string $sKind): void
+    {
+        $this->assertFalse(isLogged());
+
+        $oAccount = $this->bxAccount($sKind);
+        $aInfo = $this->bxLoginAs($sKind);
+
+        $this->assertSame((int)$oAccount->id(), (int)$aInfo['id']);
+        $this->assertSame($this->bxTestEmail($sKind), $aInfo['email']);
+        $this->assertTrue(isLogged());
+        $this->assertTrue(isMember());
+        $this->assertSame((int)$oAccount->id(), (int)getLoggedId());
+        $this->assertSame((int)$oAccount->id(), (int)BxDolSession::getInstance()->getUserId());
+
+        if ($sKind === 'admin')
+            $this->assertTrue(isAdmin());
+        else
+            $this->assertFalse(isAdmin());
+    }
+
+    #[DataProvider('providerForAccounts')]
+    public function testLogoutClearsSession(string $sKind): void
+    {
+        $this->bxLoginAs($sKind);
+        $this->assertTrue(isLogged());
+
+        bx_logout();
+        $this->bxBecomeGuest();
+
+        $this->assertFalse(isLogged());
+        $this->assertFalse(isAdmin());
+        $this->assertFalse(isMember());
+        $this->assertSame(0, (int)getLoggedId());
+        $this->assertSame(0, (int)BxDolSession::getInstance()->getUserId());
+    }
+
+    #[DataProvider('providerForAccounts')]
+    public function testLoginFormAcceptsCredentials(string $sKind): void
+    {
+        $this->bxAccount($sKind);
+        $oForm = $this->bxSubmitLoginForm($this->bxTestEmail($sKind), $this->bxTestPassword($sKind));
+
+        $this->assertTrue($oForm->isSubmitted());
+        $this->assertTrue($oForm->isSubmittedAndValid(), $oForm->getLoginError() ?: 'login form should accept ' . $sKind . ' credentials');
+
+        $oAccount = BxDolAccount::getInstance(trim($oForm->getCleanValue('ID')));
+        $this->assertInstanceOf(BxDolAccount::class, $oAccount);
+
+        $aInfo = bx_login($oAccount->id(), $oForm->getRememberMe());
+        $this->assertIsArray($aInfo);
+        $this->bxSyncSessionCookie();
+        $this->bxSetLoggedFlags((int)$aInfo['role']);
+
+        $this->assertTrue(isLogged());
+        $this->assertSame((int)$oAccount->id(), (int)getLoggedId());
+        if ($sKind === 'admin')
+            $this->assertTrue(isAdmin());
+        else
+            $this->assertFalse(isAdmin());
+    }
+
+    #[DataProvider('providerForAccounts')]
+    public function testLoginFormRejectsWrongPassword(string $sKind): void
+    {
+        $this->bxAccount($sKind);
+        $oForm = $this->bxSubmitLoginForm($this->bxTestEmail($sKind), 'not-the-password');
+
+        $this->assertTrue($oForm->isSubmitted());
+        $this->assertFalse($oForm->isSubmittedAndValid());
+        $this->assertNotSame('', $oForm->getLoginError());
+        $this->assertFalse(isLogged());
+    }
+
+    #[DataProvider('providerForAccounts')]
+    public function testServiceLogoutEndsSession(string $sKind): void
+    {
+        $this->bxLoginAs($sKind);
+        $this->assertTrue(isLogged());
+
+        $this->assertTrue(bx_srv('system', 'logout', [false], 'TemplServiceContent'));
+        $this->bxBecomeGuest();
+
+        $this->assertFalse(isLogged());
+        $this->assertSame(0, (int)getLoggedId());
+    }
+}

+ 4 - 1
tests/phpunit.xml

@@ -8,9 +8,12 @@
          failOnWarning="false"
          failOnDeprecation="false">
   <testsuites>
-    <testsuite name="AllTests">
+    <testsuite name="Units">
       <directory>./units</directory>
     </testsuite>
+    <testsuite name="Integration">
+      <directory>./integration</directory>
+    </testsuite>
   </testsuites>
   <source>
     <include>