28 Commits
v0.2.1 ... main

Author SHA1 Message Date
ch4o5
47b5a398b5 chore(release): 1.0.0-beta.3 2025-04-10 08:53:07 +00:00
李东云
aa1833d37c fix(authn): 修复不再被支持的 swoole 变量 2025-04-10 16:52:37 +08:00
ch4o5
61dbf7f537 chore(release): 1.0.0-beta.2 2024-05-21 10:39:19 +00:00
李东云
a541d7584a feat(sp): 实现了定制 entityId 等参数的功能 2024-05-21 18:38:54 +08:00
李东云
c0e298f778 build(composer): 更新依赖 2024-05-21 18:34:43 +08:00
ch4o5
d7929cbcf5 chore(release): 1.0.0-beta.1 2023-12-27 03:41:17 +00:00
李东云
1356620e2d fix(sp.sso): 修复了参数不全可能被当成 assertion 误处理的问题 2023-12-27 11:35:34 +08:00
ch4o5
12905ccf63 chore(release): 1.0.0-beta.0 2023-12-26 07:57:07 +00:00
李东云
de57588ef1 build(release): 更新到 beta字段 2023-12-26 15:56:52 +08:00
李东云
6012da8005 style: formatter 2023-12-25 17:14:38 +08:00
李东云
1013894cca build: 增加lint/prettier/unitTest 2023-12-25 17:10:21 +08:00
ch4o5
03e717138d chore(release): 1.0.0-alpha.4 2023-12-04 09:50:01 +00:00
李东云
a53164a405 build(composer): 迁移到 hyperf3.1 2023-12-04 17:46:16 +08:00
ch4o5
a00ff47c0c chore(release): 1.0.0-alpha.3 2023-10-30 08:58:47 +00:00
李东云
2c1d61e664 build(composer): 更新依赖 2023-10-30 16:58:33 +08:00
ch4o5
bd6f8af46f chore(release): 1.0.0-alpha.2 2023-10-23 12:56:16 +00:00
李东云
18d3bff2b7 ci: 修复内容为空的问题 2023-10-23 20:56:09 +08:00
ch4o5
537c296288 chore(release): 1.0.0-alpha.1 2023-10-23 12:06:09 +00:00
李东云
e742538c64 build: 引入 gitea 仓库 2023-10-23 20:04:30 +08:00
李东云
e2210e5a55 ci(gitea): 引入 actions 2023-10-23 20:04:13 +08:00
李东云
deb80b5b61 build(composer): 更新依赖 2023-08-30 17:07:47 +08:00
李东云
da35cf8442 build(release): 增加预发布版本的后缀 2023-07-25 15:05:17 +08:00
ch4o5
045b361c5a chore(release): 1.0.0-alpha.0 2023-07-25 07:04:36 +00:00
李东云
9dde7b1187 build(composer): 更新依赖到 hdk 1.0 2023-07-25 15:03:59 +08:00
ch4o5
2918b8e23a chore(release): 0.2.3 2023-03-21 05:44:32 +00:00
李东云
d994292d41 build(composer): 迁移到腾讯源,并更新依赖 2023-03-21 13:44:08 +08:00
ch4o5
f4672c5152 chore(release): 0.2.2 2023-03-21 03:12:05 +00:00
李东云
37654602d9 build(composer): 更新依赖 2023-03-21 11:09:50 +08:00
34 changed files with 5110 additions and 2780 deletions

View File

@@ -0,0 +1,28 @@
name: Release development version to registry
on:
push:
tags:
- '**.**'
jobs:
Publish on Tagged:
runs-on: ubuntu-latest
steps:
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- name: Zip files in the repository
run: |
apt-get update
apt-get install zip
zip -r dist.zip *
- name: Publish to registry
run: |
curl --user ch4o5:4fd300672472e666014314c1c94c604c634165a9 \
--upload-file ./dist.zip \
https://nest.doylee.cn/api/packages/HDK/composer?version=${{ gitea.ref_name }}
- run: echo "🍏 This job's status is ${{ job.status }}."

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
/vendor/
*.cache
*.log
runtime/
# IDE support
.idea/

View File

@@ -1,89 +1,16 @@
<?php
$header = <<<'EOF'
This file is part of Hyperf.
$finder = PhpCsFixer\Finder::create()->in([
__DIR__ . '/publish',
__DIR__ . '/src',
__DIR__ . '/tests',
]);
@link https://www.hyperf.io
@document https://hyperf.wiki
@contact group@hyperf.io
@license https://github.com/hyperf/hyperf/blob/master/LICENSE
EOF;
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@PSR2' => true,
'@Symfony' => true,
'@DoctrineAnnotation' => true,
'@PhpCsFixer' => true,
'header_comment' => [
'comment_type' => 'PHPDoc',
'header' => $header,
'separate' => 'none',
'location' => 'after_declare_strict',
],
'array_syntax' => [
'syntax' => 'short'
],
'list_syntax' => [
'syntax' => 'short'
],
'concat_space' => [
'spacing' => 'one'
],
'blank_line_before_statement' => [
'statements' => [
'declare',
],
],
'general_phpdoc_annotation_remove' => [
'annotations' => [
'author'
],
],
'ordered_imports' => [
'imports_order' => [
'class', 'function', 'const',
],
'sort_algorithm' => 'alpha',
],
'single_line_comment_style' => [
'comment_types' => [
],
],
'yoda_style' => [
'always_move_variable' => false,
'equal' => false,
'identical' => false,
],
'phpdoc_align' => [
'align' => 'left',
],
'multiline_whitespace_before_semicolons' => [
'strategy' => 'no_multi_line',
],
'constant_case' => [
'case' => 'lower',
],
'class_attributes_separation' => true,
'combine_consecutive_unsets' => true,
'declare_strict_types' => true,
'linebreak_after_opening_tag' => true,
'lowercase_static_reference' => true,
'no_useless_else' => true,
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'not_operator_with_space' => false,
'ordered_class_elements' => true,
'php_unit_strict' => false,
'phpdoc_separation' => false,
'single_quote' => true,
'standardize_not_equals' => true,
'multiline_comment_opening_closing' => true,
])
->setFinder(
PhpCsFixer\Finder::create()
->exclude('vendor')
->in(__DIR__)
)
->setUsingCache(false);
$config = new PhpCsFixer\Config();
return $config->setRules([
'@PSR12' => true,
'strict_param' => true,
'array_syntax' => ['syntax' => 'short'],
])
->setUsingCache(false)
->setFinder($finder);

View File

@@ -1,4 +1,99 @@
# 版本更新日志
## [1.0.0-beta.3](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2025-04-10)
### 🐛 Bug Fixes | Bug 修复
* **authn:** 修复不再被支持的 swoole 变量 ([aa1833d](http://124.126.16.154:8888/singularity/hyperf-saml/commit/aa1833d37c1052d322bfbb6a279ed9f6dfd6f743))
## [1.0.0-beta.2](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2024-05-21)
### 📦‍ Build System | 打包构建
* **composer:** 更新依赖 ([c0e298f](http://124.126.16.154:8888/singularity/hyperf-saml/commit/c0e298f7785cf58724fede60ccb4d1b04e096e85))
### ✨ Features | 新功能
* **sp:** 实现了定制 entityId 等参数的功能 ([a541d75](http://124.126.16.154:8888/singularity/hyperf-saml/commit/a541d7584ae874529c0b8f06f3490d85225e4d8e))
## [1.0.0-beta.1](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2023-12-27)
### 🐛 Bug Fixes | Bug 修复
* **sp.sso:** 修复了参数不全可能被当成 assertion 误处理的问题 ([1356620](http://124.126.16.154:8888/singularity/hyperf-saml/commit/1356620e2d9a9180a93c2ef0c7c819d097ee121f))
## [1.0.0-beta.0](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2023-12-26)
### 💄 Styles | 风格
* formatter ([6012da8](http://124.126.16.154:8888/singularity/hyperf-saml/commit/6012da8005ff7e9b14b5e6b44e54f8711ff0e03c))
### 📦‍ Build System | 打包构建
* **release:** 更新到 beta字段 ([de57588](http://124.126.16.154:8888/singularity/hyperf-saml/commit/de57588ef1628b72e006e2bfe7401d5332e95153))
* 增加lint/prettier/unitTest ([1013894](http://124.126.16.154:8888/singularity/hyperf-saml/commit/1013894ccafcfde578475decfdb59b91c81a727d))
## [1.0.0-alpha.4](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2023-12-04)
### 📦‍ Build System | 打包构建
* **composer:** 迁移到 hyperf3.1 ([a53164a](http://124.126.16.154:8888/singularity/hyperf-saml/commit/a53164a4059abdc21bf38c94ce6ba36a112ff118))
## [1.0.0-alpha.3](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2023-10-30)
### 📦‍ Build System | 打包构建
* **composer:** 更新依赖 ([2c1d61e](http://124.126.16.154:8888/singularity/hyperf-saml/commit/2c1d61e664a4c09b3c16ae2374552f7ccc7976be))
## [1.0.0-alpha.2](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2023-10-23)
### 👷 Continuous Integration | CI 配置
* 修复内容为空的问题 ([18d3bff](http://124.126.16.154:8888/singularity/hyperf-saml/commit/18d3bff2b7b2daece8295437dde058a8c62f9a92))
## [1.0.0-alpha.1](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2023-10-23)
### 👷 Continuous Integration | CI 配置
* **gitea:** 引入 actions ([e2210e5](http://124.126.16.154:8888/singularity/hyperf-saml/commit/e2210e5a557f8e8e17160077f64b6ebb42127bdf))
### 📦‍ Build System | 打包构建
* 引入 gitea 仓库 ([e742538](http://124.126.16.154:8888/singularity/hyperf-saml/commit/e742538c6499438e1f149ade25fb99549f4beb79))
* **composer:** 更新依赖 ([deb80b5](http://124.126.16.154:8888/singularity/hyperf-saml/commit/deb80b5b6115c1c4caa6e6c1e9594057f310eb4a))
* **release:** 增加预发布版本的后缀 ([da35cf8](http://124.126.16.154:8888/singularity/hyperf-saml/commit/da35cf84421fefb133db11e21c6840fcd4c17a70))
## [1.0.0-alpha.0](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v0.2.3...v1.0.0-alpha.0) (2023-07-25)
### 📦‍ Build System | 打包构建
* **composer:** 更新依赖到 hdk 1.0 ([9dde7b1](http://124.126.16.154:8888/singularity/hyperf-saml/commit/9dde7b118761d935007d161f21c99ffa3827d281))
### [0.2.3](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v0.2.2...v0.2.3) (2023-03-21)
### 📦‍ Build System | 打包构建
* **composer:** 迁移到腾讯源,并更新依赖 ([d994292](http://124.126.16.154:8888/singularity/hyperf-saml/commit/d994292d41aab83a1e295e6b1fd3c96a075589d3))
### [0.2.2](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v0.2.1...v0.2.2) (2023-03-21)
### 📦‍ Build System | 打包构建
* **composer:** 更新依赖 ([3765460](http://124.126.16.154:8888/singularity/hyperf-saml/commit/37654602d935518473a7c275afc54fd39c9e9ef1))
### [0.2.1](http://124.126.16.154:8888/singularity/hyperf-saml/compare/v0.2.0...v0.2.1) (2023-03-15)

View File

@@ -18,40 +18,52 @@
}
},
"require": {
"php": "~8.0",
"hyperf/config": "3.0.*",
"hyperf/constants": "3.0.*",
"hyperf/di": "3.0.*",
"hyperf/http-server": "3.0.*",
"hyperf/translation": "3.0.*",
"hyperf/validation": "3.0.*",
"php": ">=8.1",
"ext-redis": "*",
"hyperf/config": "3.1.*",
"hyperf/constants": "3.1.*",
"hyperf/di": "3.1.*",
"hyperf/http-server": "3.1.*",
"hyperf/translation": "3.1.*",
"hyperf/validation": "3.1.*",
"litesaml/lightsaml": "~3.0.0",
"singularity/hdk-core": "^0.2.9",
"singularity/hdk-auth": "^0.2.1",
"teapot/status-code": "^1.1",
"ext-redis": "*"
"singularity/hdk-core": "^1.0.0",
"singularity/hdk-auth": "^1.0.0",
"teapot/status-code": "^1.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
"friendsofhyperf/pest-plugin-hyperf": "3.1.*",
"mockery/mockery": "^1.0",
"phpstan/phpstan": "^1.0",
"phpunit/phpunit": ">=7.0",
"swoole/ide-helper": "^4.5"
},
"suggest": {
"swow/swow": "Required to create swow components."
},
"minimum-stability": "dev",
"minimum-stability": "alpha",
"prefer-stable": true,
"config": {
"optimize-autoloader": true,
"sort-packages": true,
"secure-http": false
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"scripts": {
"test": "phpunit -c phpunit.xml --colors=always",
"analyse": "phpstan analyse --memory-limit 1024M -l 0 ./src",
"cs-fix": "php-cs-fixer fix $1"
"test": [
"rm -rf runtime",
"Composer\\Config::disableProcessTimeout",
"pest --coroutine --prepend tests/bootstrap.php --colors=always"
],
"cs-fix": "php-cs-fixer fix $1 --rules=@PSR12 --allow-risky=yes",
"analyse": "phpstan analyse $1",
"ci": [
"@analyse publish/ src/ tests/",
"@cs-fix",
"@test",
"echo CI Success"
]
},
"extra": {
"hyperf": {
@@ -62,14 +74,14 @@
"bin/generate-cert"
],
"repositories": {
"nest": {
"type": "composer",
"url": "https://nest.doylee.cn/api/packages/HDK/composer"
},
"packagist": {
"type": "composer",
"url": "https://mirrors.aliyun.com/composer/"
},
"lux-map": {
"type": "composer",
"url": "https://satis.luxcreo.cn/"
}
},
"version": "0.2.1"
"version": "1.0.0-beta.3"
}

6989
composer.lock generated

File diff suppressed because it is too large Load Diff

15
phpstan.dist.neon Normal file
View File

@@ -0,0 +1,15 @@
parameters:
level: 1
reportUnmatchedIgnoredErrors: false
checkGenericClassInNonGenericObjectType: false
paths:
- publish
- src
- tests
ignoreErrors:
- '#Constant BASE_PATH not found#'
- '#Unknown parameter \$[a-zA-Z0-9]+ in call to callable Closure\.#'
- '#Property [a-zA-Z0-9\\_]+::\$[a-zA-Z0-9]+ is never written, only read\.#'
- '#Method [a-zA-Z0-9\\_]+::[a-zA-Z0-9]+\(\) is unused\.#'
- '#Method [a-zA-Z0-9\\_]+::[a-zA-Z0-9]+\(\) has parameter \$response with no value type specified in iterable type array\.#'
- '#Undefined variable: \$this#'

View File

@@ -1,15 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
backupGlobals="false"
backupStaticAttributes="false"
verbose="true"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuite name="Testsuite">
<directory>./tests/</directory>
</testsuite>
</phpunit>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="tests/bootstrap.php"
colors="true"
stopOnFailure="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd"
cacheDirectory=".phpunit.cache"
>
<testsuites>
<testsuite name="Testsuite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>./src</directory>
</include>
</source>
</phpunit>

View File

@@ -14,4 +14,4 @@ return [
'saml_request' => 'SAMLRequest is required',
'saml_response' => 'SAMLResponse is required',
],
];
];

View File

@@ -14,4 +14,4 @@ return [
'saml_request' => 'SAMLRequest 参数不能为空',
'saml_response' => 'SAMLResponse 参数不能为空',
],
];
];

View File

@@ -2,21 +2,23 @@
declare(strict_types=1);
use function Hyperf\Support\env;
return [
// 当前项目类型
'type' => 'sp', // 可选值sp/idp
// 是否支持多用户同时在线
'allow_multi_online' => true,
// IDP 相关配置
'server' => [
// common config
'idp_id' => env('IDP_ID', 'https://test-accountx.luxcreo.cn/api/v1/auth'),
// 单点登录
// idp config
// sp config
// 以下内容向服务端申请
'idp_assertion_url' => env('IDP_ASSERTION_URL', 'https://test-accountx.luxcreo.cn/api/v1/auth/assertion'),
@@ -24,16 +26,16 @@ return [
'idp_logout_url' => env('IDP_LOGOUT_URL', 'https://test-accountx.luxcreo.cn/api/v1/slo'),
//单点退出
],
// SP 相关配置
'client' => [
// sp config
// 以下内容向服务端申请
'entity_id' => env('ENTITY_ID', ''), // TODO 业务系统唯一标识
'acs_url' => env('ACS_URL', ''), // TODO 回调地址
'landing_host' =>env('LANDING_HOST', ''), // TODO 站点 host
'landing_host' => env('LANDING_HOST', ''), // TODO 站点 host
],
// 证书
'credential' => [
'enable' => false,
@@ -42,4 +44,4 @@ return [
'crt' => 'saml.crt',
'pem' => 'saml.pem',
],
];
];

View File

@@ -6,4 +6,4 @@ docker run \
-w "/srv/www" \
-v "$(pwd)":/srv/www \
-v ~/.ssh:/root/.ssh \
harbor.luxcreo.cn/library/hyperf:8.0-swoole /bin/ash
harbor.luxcreo.cn/library/hyperf:8.1-swoole /bin/ash

View File

@@ -2,4 +2,4 @@
docker run --rm -it \
-v $(pwd):/app -e "GIT_AUTHOR_NAME=ch4o5" -e "EMAIL=dongyun.li@luxcreo.ai" \
detouched/standard-version:latest $1
detouched/standard-version:latest -p=beta $1

View File

@@ -7,6 +7,7 @@
* Created on 2022/4/25
*/
declare(strict_types=1);
namespace Singularity\HyperfSaml\Constants;
use Hyperf\Constants\AbstractConstants;
@@ -23,27 +24,26 @@ use Hyperf\Constants\Annotation\Constants;
*/
class SamlErrorCode extends AbstractConstants
{
// 203 SAML 鉴权
/**
* @Message("saml_error.default")
*/
public const AUTH_SAML_ERROR = 203000;
// 20301 验证
/**
* @Message("saml_error.params.default")
*/
public const AUTH_SAML_REQUEST_PARAMS_ERROR = 2030100;
/**
* @Message("saml_error.params.saml_request")
*/
public const AUTH_SAML_REQUEST_PARAMS_SAML_REQUEST = 2030101;
/**
* @Message("saml_error.params.saml_response")
*/
public const AUTH_SAML_REQUEST_PARAMS_SAML_RESPONSE = 2030102;
}
}

View File

@@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace Singularity\HyperfSaml\Exceptions\Handler;
use Hyperf\Di\Annotation\Inject;
@@ -16,6 +17,8 @@ use Singularity\HyperfSaml\Services\Idp\AbstractLoginService;
use Singularity\HyperfSaml\Services\Idp\AbstractLogoutService;
use Throwable;
use function Hyperf\Config\config;
/**
* IDP 相关错误捕获
* Singularity\HyperfSaml\Exceptions\Handler\SamlIdpHandler@HyperfSaml
@@ -31,31 +34,31 @@ class SamlIdpHandler extends ExceptionHandler
* @var \Singularity\HyperfSaml\Services\Base
*/
private Base $base;
/**
* @Inject
* @var \Singularity\HyperfSaml\Services\Idp\AbstractLoginService
*/
private AbstractLoginService $loginService;
/**
* @Inject
* @var \Singularity\HyperfSaml\Services\Idp\AbstractLogoutService
*/
private AbstractLogoutService $logoutService;
/**
* @Inject
* @var \Hyperf\Framework\Logger\StdoutLogger
*/
private StdoutLogger $logger;
/**
* @Inject(required=false)
* @var \Hyperf\HttpServer\Contract\RequestInterface|null
*/
private ?RequestInterface $request;
/**
* @param RuntimeException $throwable
* @param \Psr\Http\Message\ResponseInterface $response
@@ -67,7 +70,7 @@ class SamlIdpHandler extends ExceptionHandler
{
// 阻止异常冒泡
$this->stopPropagation();
// IDP 相关
if ($throwable instanceof ValidationException) {
$SAMLResponse = $this->logoutService->createLogoutResponse(
@@ -84,7 +87,7 @@ class SamlIdpHandler extends ExceptionHandler
messageId: $throwable->getMessageId(),
);
}
$error_type = get_class($throwable);
$data = [
'errorCode' => $throwable->getCode(),
@@ -98,7 +101,7 @@ class SamlIdpHandler extends ExceptionHandler
'sp' => config('saml.client.entity_id'),
'messageId' => $throwable->getMessageId(),
], JSON_UNESCAPED_UNICODE);
$this->logger->error(
<<<ERROR_LOG
TYPE: $error_type
@@ -136,13 +139,13 @@ ERROR_LOG
$throwable->getRelayState(),
[]
);
$this->logger->error("location: $url");
return $response
->withStatus(302)
->withAddedHeader('Location', $url);
}
/**
* @inheritDoc
*/
@@ -150,4 +153,4 @@ ERROR_LOG
{
return $throwable instanceof RuntimeException;
}
}
}

View File

@@ -1,14 +1,16 @@
<?php
declare(strict_types=1);
namespace Singularity\HyperfSaml\Exceptions\Handler;
use Exception;
use Hyperf\Codec\Json;
use Hyperf\Di\Annotation\Inject;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\Framework\Logger\StdoutLogger;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Utils\Codec\Json;
use Lmc\HttpConstants\Header;
use Psr\Http\Message\ResponseInterface;
use Singularity\HyperfSaml\Constants\SamlErrorCode;
@@ -17,6 +19,8 @@ use Singularity\HyperfSaml\Exceptions\ValidationException;
use Teapot\StatusCode\RFC\RFC7231;
use Throwable;
use function Hyperf\Config\config;
/**
* SP 相关错误捕获
* Singularity\HyperfSaml\Exceptions\Handler\SamlIdpHandler@HyperfSaml
@@ -29,45 +33,45 @@ class SamlSpHandler extends ExceptionHandler
{
/**
* @Inject
* @var \Hyperf\Framework\Logger\StdoutLogger
* @var StdoutLogger
*/
private StdoutLogger $logger;
/**
* @Inject(required=false)
* @var \Hyperf\HttpServer\Contract\RequestInterface|null
* @var RequestInterface|null
*/
private ?RequestInterface $request;
/**
* @param RuntimeException $throwable
* @param \Psr\Http\Message\ResponseInterface $response
* @param RuntimeException $throwable
* @param ResponseInterface $response
*
* @return \Psr\Http\Message\ResponseInterface
* @throws \Exception
* @return ResponseInterface
* @throws Exception
*/
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
{
// 阻止异常冒泡
$this->stopPropagation();
$restful = config('common.response.restful');
$code_name = config('common.response.code_name');
$message_name = config('common.response.message_name');
$this->request?->url();
$error_type = get_class($throwable);
$is_testing = config('app_status') === true;
$is_debug = $this->request?->hasHeader('Postman-Token')
|| $this->request?->header('User-Agent') === 'apifox/2.1.8 (https://www.apifox.cn)';
$data = [
$code_name => $throwable->getCode(),
$message_name => $throwable->getMessage(),
];
// 验证失败
if ($throwable instanceof ValidationException) {
$code = $throwable->getCode();
@@ -82,8 +86,8 @@ class SamlSpHandler extends ExceptionHandler
$data['availableValue'] = $throwable->getAvailableValue();
}
}
// 整理日志所需的数据
$request_time = date('Y-m-d H:i:s');
$request_headers = $this->request->getHeaders();
@@ -93,20 +97,19 @@ class SamlSpHandler extends ExceptionHandler
'sp' => config('saml.client.entity_id'),
'messageId' => $throwable->getMessageId(),
], JSON_UNESCAPED_UNICODE);
$response = $response->withHeader(
Header::CONTENT_TYPE,
'application/json; charset=utf-8'
);
if ($is_debug && $is_testing) {
$data['trace'] = [
'errorType' => $error_type,
'errorTrack' => $throwable->getTrace(),
];
}
$cookies = json_encode($this->request->getCookieParams(), JSON_UNESCAPED_UNICODE);
$this->logger->error(
<<<ERROR_LOG
TYPE: $error_type
@@ -139,11 +142,10 @@ TRACE:
ERROR_LOG
);
$data = Json::encode($data);
if ($restful) {
$response = $response->withStatus(
$status_code ??
$throwable->status ??
$throwable->statusCode ??
RFC7231::INTERNAL_SERVER_ERROR
@@ -154,7 +156,7 @@ ERROR_LOG
new SwooleStream($data)
);
}
/**
* @inheritDoc
*/
@@ -162,4 +164,4 @@ ERROR_LOG
{
return $throwable instanceof RuntimeException;
}
}
}

View File

@@ -1,11 +1,11 @@
<?php
declare(strict_types=1);
namespace Singularity\HyperfSaml\Exceptions\Logout;
use Singularity\HyperfSaml\Exceptions\ValidationException as SAMLValidationException;
class ValidationException extends SAMLValidationException
{
}
}

View File

@@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace Singularity\HyperfSaml\Exceptions;
use Throwable;
@@ -17,7 +18,7 @@ class RuntimeException extends \RuntimeException
) {
parent::__construct($message, $code, $previous);
}
/**
* @return string
*/
@@ -25,7 +26,7 @@ class RuntimeException extends \RuntimeException
{
return $this->statusCode;
}
/**
* @return string
*/
@@ -33,7 +34,7 @@ class RuntimeException extends \RuntimeException
{
return $this->relayState;
}
/**
* @return string
*/
@@ -41,4 +42,4 @@ class RuntimeException extends \RuntimeException
{
return $this->messageId;
}
}
}

View File

@@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace Singularity\HyperfSaml\Exceptions;
// use App\Model\ServiceProvider;
@@ -29,17 +30,17 @@ class ValidationException extends RuntimeException
SamlConstants::STATUS_INVALID_NAME_ID_POLICY,
$messageId,
$relayState,
$message ?? SamlErrorCode::getMessage($code) ,
$message ?? SamlErrorCode::getMessage($code),
$code,
$previous
);
}
public function getFieldName(): string
{
return $this->field;
}
/**
* @return mixed
*/
@@ -47,7 +48,7 @@ class ValidationException extends RuntimeException
{
return $this->currentValue;
}
/**
* @return array
*/
@@ -55,4 +56,4 @@ class ValidationException extends RuntimeException
{
return $this->availableValue;
}
}
}

View File

@@ -52,14 +52,14 @@ abstract class AbstractService
try {
$bindingFactory = new BindingFactory();
$binding = $bindingFactory->getBindingByRequest($requestInstance);
// We prepare a message context to receive our SAML Request message.
$messageContext = new MessageContext();
// The received method fills in the messageContext with the SAML Request data.
/** @var \LightSaml\Model\Protocol\Response $response */
$binding->receive($requestInstance, $messageContext);
return $messageContext;
} catch (InvalidArgumentException $exception) {
throw new ValidationException(
@@ -68,7 +68,7 @@ abstract class AbstractService
);
}
}
/**
* 响应给 ACS
*
@@ -93,10 +93,10 @@ abstract class AbstractService
}
$messageContext = new MessageContext();
$messageContext->setMessage($responseInstance);
$bindingFactory = new BindingFactory();
$redirectBinding = $bindingFactory->create($bindingType);
// Ensure we include the RelayState.
$message = $messageContext->getMessage();
$rs = $message->getRelayState();
@@ -104,29 +104,29 @@ abstract class AbstractService
$message->setRelayState($relayState);
$messageContext->setMessage($message);
}
// Return the Response.
/** @var \Symfony\Component\HttpFoundation\RedirectResponse $httpResponse */
$httpResponse = $redirectBinding->send($messageContext);
$url_parts = parse_url($httpResponse->getTargetUrl());
$query = [];
parse_str($url_parts['query'], $query);
// $query['Signature'] = $this->base64Wrapper->encode($query['Signature']);
$query += $extraParams;
$url_parts['query'] = http_build_query($query);
$url_parts['port'] ??= '80';
if ($url_parts['port'] !== '80') {
$url_parts['host'] .= ':' . $url_parts['port'];
}
$url = $url_parts['scheme'] . '://' . $url_parts['host'] . $url_parts['path'] . '?' . $url_parts['query'];
if (!$returnHtml) {
return $url;
}
return sprintf(
'<!DOCTYPE html>
<html lang="">
@@ -142,7 +142,7 @@ abstract class AbstractService
);
// return $httpResponse->setTargetUrl($url)->getContent();
}
public function requestParser($request): Request
{
$params = $request->getQueryParams();
@@ -152,7 +152,7 @@ abstract class AbstractService
$params,
);
}
/**
* Retrieves the certificate from the IdP.
*
@@ -162,7 +162,7 @@ abstract class AbstractService
{
return X509Certificate::fromFile('cert/server.crt');
}
/**
* Retrieves the private key from the Idp.
*
@@ -172,4 +172,4 @@ abstract class AbstractService
{
return KeyHelper::createPrivateKey('cert/server.key', '', true);
}
}
}

View File

@@ -37,6 +37,8 @@ use RobRichards\XMLSecLibs\XMLSecurityKey;
use Singularity\HDK\Auth\Resource\User;
use Throwable;
use function Hyperf\Config\config;
/**
* Singularity\HyperfSaml\Lib\Base@HyperfSaml
*
@@ -424,4 +426,4 @@ class Base extends AbstractService
{
return $response->getRelayState();
}
}
}

View File

@@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace Singularity\HyperfSaml\Services\Idp;
use DateTime;
@@ -35,7 +36,7 @@ class AbstractLoginService extends AbstractService
* @var \Hyperf\Framework\Logger\StdoutLogger
*/
private StdoutLogger $logger;
/**
* Constructs a SAML Response.
*
@@ -55,7 +56,7 @@ class AbstractLoginService extends AbstractService
$statusCode = isset($user)
? SamlConstants::STATUS_SUCCESS
: SamlConstants::STATUS_NO_PASSIVE;
$response = new Response();
// 生成断言
$assertion = $this->assertionBuilder(
@@ -64,12 +65,12 @@ class AbstractLoginService extends AbstractService
$messageId,
$sp?->acsUrl ?? ''
);
// 加密
$certificate = (new X509Certificate())->loadPem($sp->secret);
$encryptedAssertion = new EncryptedAssertionWriter();
$encryptedAssertion->encrypt($assertion, KeyHelper::createPublicKey($certificate));
$response
// ->addAssertion($assertion)
->addEncryptedAssertion($encryptedAssertion)
@@ -79,21 +80,21 @@ class AbstractLoginService extends AbstractService
->setID(Helper::generateID())
->setIssueInstant(new DateTime())
->setDestination($sp->acsUrl);
// Sign the response.
// $response->setSignature(
// new SignatureWriter($this->getCertificate(), $this->getPrivateKey())
// );
// Preparing the response XML
$serializationContext = new SerializationContext();
// Serialize to XML.
$response->serialize($serializationContext->getDocument(), $serializationContext);
return $response;
}
/**
* 断言构建器
*
@@ -111,33 +112,33 @@ class AbstractLoginService extends AbstractService
string $acsUrl = ''
): Assertion {
$assertion = new Assertion();
$this->logger->info('生成 NameID: ' . $user?->uid ?? 'nil');
// 构建基本信息
$name_id = new NameID(
$user?->uid ?? 'nil',
SamlConstants::NAME_ID_FORMAT_PERSISTENT
);
$subject = new Subject();
$subject_confirmation = new SubjectConfirmation();
$subject_confirmation_data = new SubjectConfirmationData();
$subject_confirmation_data = $subject_confirmation_data
->setInResponseTo($messageId) # SAML 请求的标识
->setNotOnOrAfter(new DateTime('+1 MINUTE')) # 有效期 1 分钟内
->setRecipient($acsUrl); # 断言消费服务的地址
/** @var SubjectConfirmation 验证数据 $subject_confirmation */
$subject_confirmation = $subject_confirmation
->setMethod(SamlConstants::CONFIRMATION_METHOD_BEARER)
->setSubjectConfirmationData($subject_confirmation_data);
/** @var Subject 主题/标题 $subject */
$subject = $subject
->setNameID($name_id)
->addSubjectConfirmation($subject_confirmation);
// 配置生效条件
$conditions = new Conditions();
$conditions = $conditions
@@ -146,18 +147,18 @@ class AbstractLoginService extends AbstractService
->addItem(
new AudienceRestriction([$issuerId])# 受众限制
);
$assertion = $assertion
->setId(Helper::generateID()) # 本次的 MessageID
->setIssueInstant(new DateTime()) # 当前时间戳
->setIssuer(new Issuer($this->idpId)) # 发信人——IDP的entityID
->setSubject($subject)# 设置主题/标题
->setConditions($conditions);
// 补充更多内容
if (isset($user)) {
$attribute_statement = new AttributeStatement();
// uid
$attribute_statement = $attribute_statement
->addAttribute(
@@ -172,7 +173,7 @@ class AbstractLoginService extends AbstractService
$user->uid ?? null
)
);
// secEmail
$attribute_statement = $attribute_statement
->addAttribute(
@@ -187,7 +188,7 @@ class AbstractLoginService extends AbstractService
$user->secEmail ?? null
)
);
// username
$attribute_statement = $attribute_statement
->addAttribute(
@@ -202,7 +203,7 @@ class AbstractLoginService extends AbstractService
$user->username ?? null
)
);
// 密码
$attribute_statement = $attribute_statement
->addAttribute(
@@ -216,7 +217,7 @@ class AbstractLoginService extends AbstractService
!empty($user->password)
)
);
//其他
$attribute_statement = $attribute_statement
->addAttribute(
@@ -236,11 +237,11 @@ class AbstractLoginService extends AbstractService
$user->avatar
)
);
$assertion->addItem($attribute_statement);
}
return $assertion;
}
}
}

View File

@@ -16,6 +16,8 @@ use Singularity\HDK\Core\Constants\CommonErrorCode;
use Singularity\HyperfSaml\Exceptions\Logout\ValidationException;
use Singularity\HyperfSaml\Services\AbstractService;
use function Hyperf\Config\config;
class AbstractLogoutService extends AbstractService
{
/**
@@ -153,4 +155,4 @@ class AbstractLogoutService extends AbstractService
return $user;
}
}
}

View File

@@ -31,6 +31,8 @@ use Singularity\HyperfSaml\Exceptions\RuntimeException;
use Singularity\HyperfSaml\Exceptions\ValidationException;
use Singularity\HyperfSaml\Services\Base;
use function Hyperf\Config\config;
/**
* 断言操作类
* 用于获取用户登录状态
@@ -43,12 +45,12 @@ use Singularity\HyperfSaml\Services\Base;
class Assertion
{
public function __construct(
private Base $base,
private RequestInterface $request,
private ResponseInterface $response,
private AuthenticationInterface $authentication,
private Redis $redis,
private StdoutLoggerInterface $stdoutLogger
private readonly Base $base,
private readonly RequestInterface $request,
private readonly ResponseInterface $response,
private readonly AuthenticationInterface $authentication,
private readonly Redis $redis,
private readonly StdoutLoggerInterface $stdoutLogger
) {
}
@@ -181,4 +183,4 @@ class Assertion
)->withHeader(Header::CONTENT_TYPE, 'text/html');
// return $this->response->redirect($landing_url, RFC7231::FOUND);
}
}
}

View File

@@ -8,9 +8,9 @@
*/
declare(strict_types=1);
namespace Singularity\HyperfSaml\Services\Sp;
class MetadataProfile
{
}
}

View File

@@ -15,10 +15,13 @@ use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Hyperf\Redis\Redis;
use Psr\Http\Message\ResponseInterface as PsrResponseInterface;
use RedisException;
use Singularity\HDK\Auth\Services\AuthenticationInterface;
use Singularity\HyperfSaml\Services\Base;
use Teapot\StatusCode\RFC\RFC7231;
use function Hyperf\Config\config;
/**
* 单点退出登录
* Singularity\HyperfSaml\Services\Sp\Slo@HyperfSaml
@@ -30,58 +33,65 @@ use Teapot\StatusCode\RFC\RFC7231;
class Slo
{
public function __construct(
private Base $base,
private RequestInterface $request,
private ResponseInterface $response,
private AuthenticationInterface $authentication,
private Redis $redis
private readonly Base $base,
private readonly RequestInterface $request,
private readonly ResponseInterface $response,
private readonly AuthenticationInterface $authentication,
private readonly Redis $redis,
) {
}
/**
* 重定向方式退出单点登录
*
* @param string $uid
* @param string $originToken
* @param string $uid
* @param string $originToken
* @param string|null $idpId
* @param string|null $entityId
*
* @return \Psr\Http\Message\ResponseInterface
* @return PsrResponseInterface
*/
public function redirect(string $uid, string $originToken): PsrResponseInterface
{
$idpId = config('saml.server.idp_logout_url');
$issuer = config('saml.client.entity_id');
public function redirect(
string $uid,
string $originToken,
?string $entityId = null,
?string $idpId = null,
): PsrResponseInterface {
$idpId ??= config('saml.server.idp_logout_url');
$entityId ??= config('saml.client.entity_id');
$this->authentication->invalidByToken();
$relayState = $this->request->query('RelayState', '');
$url = $this->base->createLogoutRequest(
uid: $uid,
token: $originToken,
idpID: $idpId,
issuer: $issuer,
issuer: $entityId,
relayState: $relayState
);
return $this->response->redirect($url, RFC7231::FOUND);
}
/**
* 回调方式退出单点登录IDP 通知 SP)
*
* @return \Psr\Http\Message\ResponseInterface Account 只根据 StatusCode 判断, 符合 [200, 300) 即可
* @return PsrResponseInterface Account 只根据 StatusCode 判断, 符合 [200, 300) 即可
* @throws RedisException
*/
public function callback(): PsrResponseInterface
{
$redis_prefix = config('common.redis.prefix');
$uid = $this->request->query('uid');
$originToken = $this->request->query('token');
if (empty($originToken)) {
$this->authentication->invalidByUser($uid);
} else {
$key = "{$redis_prefix}user:token_map:$uid";
$token = $this->redis->hGet($key, $originToken);
$this->redis->hDel($key, $originToken);
if ($token) {
@@ -90,4 +100,4 @@ class Slo
}
return $this->response->raw('')->withStatus(RFC7231::NO_CONTENT);
}
}
}

View File

@@ -1,4 +1,5 @@
<?php
/**
* Sso.php@HyperfSaml
*
@@ -11,19 +12,21 @@ declare(strict_types=1);
namespace Singularity\HyperfSaml\Services\Sp;
use Hyperf\Utils\Codec\Json;
use Singularity\HDK\Core\Constants\CommonErrorCode;
use Singularity\HDK\Core\Exceptions\Forbidden;
use Swoole\Exception;
use Hyperf\Codec\Json;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Hyperf\Redis\Redis;
use Psr\Http\Message\ResponseInterface as PsrResponseInterface;
use RedisException;
use Singularity\HDK\Core\Constants\CommonErrorCode;
use Singularity\HDK\Core\Exceptions\Forbidden;
use Singularity\HyperfSaml\Services\Base;
use Swoole\Exception;
use Teapot\StatusCode\RFC\RFC7231;
use function Hyperf\Config\config;
/**
* SP 单点登录
* Singularity\HyperfSaml\Sp\Sso@HyperfSaml
@@ -34,54 +37,68 @@ use Teapot\StatusCode\RFC\RFC7231;
*/
class Sso
{
private string $idpId;
private string $idpAssertionUrl;
private string $acsUrl;
private string $issuer;
public function __construct(
private Base $base,
private RequestInterface $request,
private ResponseInterface $response,
private Redis $redis,
private StdoutLoggerInterface $stdoutLogger
) {
$this->idpId = config('saml.server.idp_id');
$this->idpAssertionUrl = config('saml.server.idp_assertion_url');
$this->issuer = config('saml.client.entity_id');
$this->acsUrl = config('saml.client.acs_url');
}
private readonly Base $base,
private readonly RequestInterface $request,
private readonly ResponseInterface $response,
private readonly StdoutLoggerInterface $stdoutLogger,
private readonly ?Redis $redis,
) {}
/**
* 自动重定向获取用户登录状态
*
* @return \Psr\Http\Message\ResponseInterface
* @param string|null $entityId
* @param string|null $acsUrl
* @param string|null $idpAssertionUrl
*
* @return PsrResponseInterface
*/
public function redirectSsoStatus(): PsrResponseInterface
{
public function redirectSsoStatus(
?string $entityId = null,
?string $acsUrl = null,
?string $idpAssertionUrl = null,
): PsrResponseInterface {
$idpAssertionUrl ??= config('saml.server.idp_assertion_url');
$entityId ??= config('saml.client.entity_id');
$acsUrl ??= config('saml.client.acs_url');
$url = $this->base->createSamlRequest(
idpID: $this->idpAssertionUrl,
acsUrl: $this->acsUrl,
issuer: $this->issuer,
idpID: $idpAssertionUrl,
acsUrl: $acsUrl,
issuer: $entityId,
);
return $this->response->redirect($url, RFC7231::FOUND);
}
/**
* 自动重定向到单点登录
*
* @return \Psr\Http\Message\ResponseInterface
* @param string|null $entityId
* @param string|null $acsUrl
* @param string|null $idpId
*
* @return PsrResponseInterface
*/
public function redirectSso(): PsrResponseInterface
{
$language = $this->request->query('language') ?? config('language') ?? 'zh_CN';
$relayState = $this->request->query('RelayState', '');
public function redirectSso(
?string $entityId = null,
?string $acsUrl = null,
?string $idpId = null,
): PsrResponseInterface {
$idpId ??= config('saml.server.idp_id');
$entityId ??= config('saml.client.entity_id');
$acsUrl ??= config('saml.client.acs_url');
$language = $this->request->query(
'language',
config('translation.locale') ?? 'en'
);
$relayState = $this->request->query('RelayState', '/');
$url = $this->base->createSamlRequest(
idpID: $this->idpId,
acsUrl: $this->acsUrl,
issuer: $this->issuer,
idpID: $idpId,
acsUrl: $acsUrl,
issuer: $entityId,
relayState: $relayState,
exactArguments: [
'language' => $language,
@@ -89,7 +106,7 @@ class Sso
);
return $this->response->redirect($url, RFC7231::FOUND);
}
/**
* 验证用户同时在多个设备登录
*
@@ -97,26 +114,26 @@ class Sso
* @param $token
*
* @return void
* @throws \Singularity\HDK\Utils\Exceptions\Forbidden
* @throws \RedisException
* @throws Forbidden
* @throws RedisException
* @throws \Exception
*/
public function checkMultiDeviceOnline($uid, $token): void
{
$allow_multi_online = config('saml.allow_multi_online');
// 只对限制为单设备登录的业务进行验证
if ($allow_multi_online) {
return;
}
$redis_prefix = config('common.redis.prefix');
$key = "{$redis_prefix}user:token_map:{$uid}";
$key = "{$redis_prefix}user:token_map:$uid";
$type = $this->redis->type($key);
try {
$this->stdoutLogger->info('user_token redis type: ' . $type);
switch ($type) {
case SWOOLE_REDIS_TYPE_STRING:
case 1:
// 旧的值
// 理论上只有刚切换的时候才会有此情况
$last_token = $this->redis->get($key);
@@ -124,19 +141,19 @@ class Sso
throw new Exception(code: CommonErrorCode::AUTH_SESSION_CREATED_AT_ERROR);
}
break;
case SWOOLE_REDIS_TYPE_HASH:
case 5:
$origin_token = $this->redis->hKeys($key);
$this->stdoutLogger->info('user_token redis keys: ' . Json::encode($origin_token));
$origin_token = array_pop($origin_token);
$this->stdoutLogger->info('user_token redis key: ' . $origin_token);
$latest_token = $this->redis->hGet($key, $origin_token);
$this->stdoutLogger->info('user_token redis value isset: ' . $latest_token);
if (empty($latest_token) || $latest_token !== $token) {
throw new Exception(code: CommonErrorCode::AUTH_SESSION_CREATED_AT_ERROR);
}
break;
case SWOOLE_REDIS_TYPE_NOT_FOUND:
case 0:
break;
default:
throw new \Exception(code: CommonErrorCode::SERVER_ERROR);
@@ -144,8 +161,8 @@ class Sso
} catch (Exception $e) {
$code = $e->getCode();
$message = CommonErrorCode::getMessage($code);
throw new Forbidden(code: $code, message: $message);
}
}
}
}

View File

@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Cases;
use PHPUnit\Framework\TestCase;
/**
* Class AbstractTestCase.
*/
abstract class AbstractTestCase extends TestCase
{
}

View File

@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Cases;
/**
* @internal
* @coversNothing
*/
class ExampleTest extends AbstractTestCase
{
public function testExample()
{
$this->assertTrue(true);
}
}

View File

@@ -0,0 +1,5 @@
<?php
test('example', function () {
expect(true)->toBeTrue();
});

45
tests/Pest.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "uses()" function to bind a different classes or traits.
|
*/
// uses(Tests\TestCase::class)->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 something()
{
// ..
}*/

10
tests/TestCase.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace Tests;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}

View File

@@ -0,0 +1,5 @@
<?php
test('example', function () {
expect(true)->toBeTrue();
});

View File

@@ -1,12 +1,31 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
require_once dirname(dirname(__FILE__)) . '/vendor/autoload.php';
use Hyperf\Context\ApplicationContext;
use Hyperf\Di\ClassLoader;
use Hyperf\Di\Container;
use Hyperf\Di\Definition\DefinitionSource;
use Psr\Container\ContainerInterface;
use Swoole\Runtime;
ini_set('display_errors', 'on');
ini_set('display_startup_errors', 'on');
error_reporting(E_ALL);
date_default_timezone_set('Asia/Shanghai');
!defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));
!defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', SWOOLE_HOOK_ALL);
Runtime::enableCoroutine(true);
require BASE_PATH . '/vendor/autoload.php';
ClassLoader::init();
$container = new Container(new DefinitionSource([]));
if (!$container instanceof ContainerInterface) {
throw new RuntimeException('The dependency injection container is invalid.');
}
$container = ApplicationContext::setContainer($container);