Реализована логика oAuth авторизации приложений, добавлен Redis, удалены лишние тесты, пофикшены старые.

This commit is contained in:
ErickSkrauch 2016-02-14 20:50:10 +03:00
parent 59addfac07
commit f5f93ddef1
52 changed files with 1752 additions and 317 deletions

View File

@ -13,8 +13,9 @@ return [
'controllerNamespace' => 'api\controllers',
'components' => [
'user' => [
'identityClass' => 'common\models\Account',
'identityClass' => \common\models\Account::class,
'enableAutoLogin' => true,
'loginUrl' => null,
],
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
@ -42,6 +43,10 @@ return [
'response' => [
'format' => \yii\web\Response::FORMAT_JSON,
],
'oauth' => [
'class' => \common\components\oauth\Component::class,
'grantTypes' => ['authorization_code'],
],
],
'params' => $params,
];

View File

@ -2,12 +2,14 @@
namespace api\controllers;
use api\traits\ApiNormalize;
use Yii;
/**
* @property \common\models\Account|null $account
*/
class Controller extends \yii\rest\Controller {
use ApiNormalize;
public $enableCsrfValidation = true;
public function behaviors() {
$parentBehaviors = parent::behaviors();
// xml нам не понадобится
@ -16,4 +18,11 @@ class Controller extends \yii\rest\Controller {
return $parentBehaviors;
}
/**
* @return \common\models\Account|null
*/
public function getAccount() {
return Yii::$app->getUser()->getIdentity();
}
}

View File

@ -0,0 +1,219 @@
<?php
namespace api\controllers;
use common\components\oauth\Exception\AcceptRequiredException;
use common\components\oauth\Exception\AccessDeniedException;
use common\models\OauthClient;
use League\OAuth2\Server\Exception\OAuthException;
use Yii;
use yii\filters\AccessControl;
use yii\helpers\ArrayHelper;
class OauthController extends Controller {
public function behaviors() {
return array_merge(parent::behaviors(), [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'actions' => ['validate'],
'allow' => true,
],
[
'actions' => ['complete'],
'allow' => true,
'roles' => ['@'],
],
],
],
]);
}
public function verbs() {
return [
'validate' => ['GET'],
'complete' => ['POST'],
];
}
/**
* @return \League\OAuth2\Server\AuthorizationServer
*/
protected function getServer() {
/** @var \common\components\oauth\Component $oauth */
$oauth = Yii::$app->get('oauth');
return $oauth->authServer;
}
/**
* @return \League\OAuth2\Server\Grant\AuthCodeGrant
*/
protected function getGrantType() {
return $this->getServer()->getGrantType('authorization_code');
}
/**
* Запрос, который должен проверить переданные параметры oAuth авторизации
* и сформировать ответ для нашего приложения на фронте
*
* Входными данными является стандартный список GET параметров по стандарту oAuth:
* $_GET = [
* client_id,
* redirect_uri,
* response_type,
* scope,
* state,
* ]
*
* Кроме того можно передать значения description для переопределения описания приложения.
*
* @return array|\yii\web\Response
*/
public function actionValidate() {
try {
$authParams = $this->getGrantType()->checkAuthorizeParams();
/** @var \League\OAuth2\Server\Entity\ClientEntity $client */
$client = $authParams['client'];
/** @var \common\models\OauthClient $clientModel */
$clientModel = OauthClient::findOne($client->getId());
$response = $this->buildSuccessResponse(
Yii::$app->request->getQueryParams(),
$clientModel,
$authParams['scopes']
);
} catch (OAuthException $e) {
$response = $this->buildErrorResponse($e);
}
return $response;
}
/**
* Метод выполняется генерацию авторизационного кода (auth_code) и формирование ссылки
* для дальнейшнешл редиректа пользователя назад на сайт клиента
*
* Входными данными является всё те же параметры, что были необходимы для валидации:
* $_GET = [
* client_id,
* redirect_uri,
* response_type,
* scope,
* state,
* ];
* А также поле accept, которое показывает, что пользователь нажал на кнопку "Принять". Если поле присутствует,
* то оно будет интерпретироваться как любое приводимое к false значение. В ином случае, значение будет
* интерпретировано, как положительный исход.
*
* @return array|\yii\web\Response
*/
public function actionComplete() {
$grant = $this->getGrantType();
try {
$authParams = $grant->checkAuthorizeParams();
$account = $this->getAccount();
/** @var \League\OAuth2\Server\Entity\ClientEntity $client */
$client = $authParams['client'];
/** @var \common\models\OauthClient $clientModel */
$clientModel = OauthClient::findOne($client->getId());
if (!$account->canAutoApprove($clientModel, $authParams['scopes'])) {
$isAccept = Yii::$app->request->post('accept');
if ($isAccept === null) {
throw new AcceptRequiredException();
}
if (!$isAccept) {
throw new AccessDeniedException($authParams['redirect_uri']);
}
}
$redirectUri = $grant->newAuthorizeRequest('user', $account->id, $authParams);
$response = [
'success' => true,
'redirectUri' => $redirectUri,
];
} catch (OAuthException $e) {
$response = $this->buildErrorResponse($e);
}
return $response;
}
/**
* Метод выполняется сервером приложения, которому был выдан auth_token.
*
* Входными данными является стандартный список GET параметров по стандарту oAuth:
* $_GET = [
* client_id,
* client_secret,
* redirect_uri,
* code|refresh_token,
* grant_type,
* ]
*
* @return array
*/
public function actionIssueToken() {
try {
$response = $this->getServer()->issueAccessToken();
} catch (OAuthException $e) {
Yii::$app->response->statusCode = $e->httpStatusCode;
$response = [
'error' => $e->errorType,
'message' => $e->getMessage(),
];
}
return $response;
}
/**
* @param array $queryParams
* @param OauthClient $clientModel
* @param \League\OAuth2\Server\Entity\ScopeEntity[] $scopes
*
* @return array
*/
private function buildSuccessResponse($queryParams, OauthClient $clientModel, $scopes) {
return [
'success' => true,
// Возвращаем только те ключи, которые имеют реальное отношение к oAuth параметрам
'oAuth' => array_intersect_key($queryParams, array_flip([
'client_id',
'redirect_uri',
'response_type',
'scope',
'state',
])),
'client' => [
'id' => $clientModel->id,
'name' => $clientModel->name,
'description' => ArrayHelper::getValue($queryParams, 'description', $clientModel->description),
],
'session' => [
'scopes' => array_keys($scopes),
],
];
}
private function buildErrorResponse(OAuthException $e) {
$response = [
'success' => false,
'error' => $e->errorType,
'parameter' => $e->parameter,
'statusCode' => $e->httpStatusCode,
];
if ($e->shouldRedirect()) {
$response['redirectUri'] = $e->getRedirectUri();
}
if ($e->httpStatusCode !== 200) {
Yii::$app->response->setStatusCode($e->httpStatusCode);
}
return $response;
}
}

View File

@ -18,7 +18,7 @@ class RegistrationForm extends BaseApiForm {
public function rules() {
return [
['rulesAgreement', 'boolean', 'message' => 'error.you_must_accept_rules'],
['rulesAgreement', 'required', 'message' => 'error.you_must_accept_rules'],
[[], ReCaptchaValidator::class, 'message' => 'error.captcha_invalid', 'when' => !YII_ENV_TEST],
['username', 'filter', 'filter' => 'trim'],

View File

@ -1,58 +0,0 @@
<?php
namespace api\models;
use common\models\Account;
use yii\base\Model;
use Yii;
/**
* Signup form
*/
class SignupForm extends Model
{
public $username;
public $email;
public $password;
/**
* @inheritdoc
*/
public function rules()
{
return [
['username', 'filter', 'filter' => 'trim'],
['username', 'required'],
['username', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This username has already been taken.'],
['username', 'string', 'min' => 2, 'max' => 255],
['email', 'filter', 'filter' => 'trim'],
['email', 'required'],
['email', 'email'],
['email', 'string', 'max' => 255],
['email', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This email address has already been taken.'],
['password', 'required'],
['password', 'string', 'min' => 6],
];
}
/**
* Signs user up.
*
* @return Account|null the saved model or null if saving fails
*/
public function signup()
{
if ($this->validate()) {
$user = new Account();
$user->email = $this->email;
$user->setPassword($this->password);
$user->generateAuthKey();
if ($user->save()) {
return $user;
}
}
return null;
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace common\components\oauth;
use common\components\oauth\Storage\Redis\AuthCodeStorage;
use common\components\oauth\Storage\Yii2\AccessTokenStorage;
use common\components\oauth\Storage\Yii2\ClientStorage;
use common\components\oauth\Storage\Yii2\ScopeStorage;
use common\components\oauth\Storage\Yii2\SessionStorage;
use League\OAuth2\Server\AuthorizationServer;
use yii\base\InvalidConfigException;
/**
* @property AuthorizationServer $authServer
*/
class Component extends \yii\base\Component {
/**
* @var AuthorizationServer
*/
private $_authServer;
/**
* @var string[]
*/
public $grantTypes = [];
/**
* @var array grant type => class
*/
public $grantMap = [
'authorization_code' => 'League\OAuth2\Server\Grant\AuthCodeGrant',
'client_credentials' => 'League\OAuth2\Server\Grant\ClientCredentialsGrant',
'password' => 'League\OAuth2\Server\Grant\PasswordGrant',
'refresh_token' => 'League\OAuth2\Server\Grant\RefreshTokenGrant'
];
public function getAuthServer() {
if ($this->_authServer === null) {
$authServer = new AuthorizationServer();
$authServer
->setAccessTokenStorage(new AccessTokenStorage())
->setClientStorage(new ClientStorage())
->setScopeStorage(new ScopeStorage())
->setSessionStorage(new SessionStorage())
->setAuthCodeStorage(new AuthCodeStorage())
->setScopeDelimiter(',');
$this->_authServer = $authServer;
foreach ($this->grantTypes as $grantType) {
if (!array_key_exists($grantType, $this->grantMap)) {
throw new InvalidConfigException('Invalid grant type');
}
$grant = new $this->grantMap[$grantType]();
$this->_authServer->addGrantType($grant);
}
}
return $this->_authServer;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace common\components\oauth\Entity;
use League\OAuth2\Server\Entity\EntityTrait;
use League\OAuth2\Server\Entity\SessionEntity;
class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity {
use EntityTrait;
protected $sessionId;
public function getSessionId() {
return $this->sessionId;
}
/**
* @inheritdoc
* @return static
*/
public function setSession(SessionEntity $session) {
parent::setSession($session);
$this->sessionId = $session->getId();
return $this;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace common\components\oauth\Entity;
use League\OAuth2\Server\Entity\EntityTrait;
use League\OAuth2\Server\Entity\SessionEntity;
class AuthCodeEntity extends \League\OAuth2\Server\Entity\AuthCodeEntity {
use EntityTrait;
protected $sessionId;
public function getSessionId() {
return $this->sessionId;
}
/**
* @inheritdoc
* @return static
*/
public function setSession(SessionEntity $session) {
parent::setSession($session);
$this->sessionId = $session->getId();
return $this;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace common\components\oauth\Entity;
use League\OAuth2\Server\Entity\ClientEntity;
use League\OAuth2\Server\Entity\EntityTrait;
class SessionEntity extends \League\OAuth2\Server\Entity\SessionEntity {
use EntityTrait;
protected $clientId;
public function getClientId() {
return $this->clientId;
}
/**
* @inheritdoc
* @return static
*/
public function associateClient(ClientEntity $client) {
parent::associateClient($client);
$this->clientId = $client->getId();
return $this;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace common\components\oauth\Exception;
use League\OAuth2\Server\Exception\OAuthException;
class AcceptRequiredException extends OAuthException {
public $httpStatusCode = 401;
/**
* {@inheritdoc}
*/
public $errorType = 'accept_required';
/**
* {@inheritdoc}
*/
public function __construct() {
parent::__construct('Client must accept authentication request.');
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace common\components\oauth\Exception;
class AccessDeniedException extends \League\OAuth2\Server\Exception\AccessDeniedException {
public function __construct($redirectUri = null) {
parent::__construct();
$this->redirectUri = $redirectUri;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace common\components\oauth\Storage\Redis;
use common\components\oauth\Entity\AuthCodeEntity;
use common\components\redis\Key;
use common\components\redis\Set;
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\AuthCodeInterface;
class AuthCodeStorage extends AbstractStorage implements AuthCodeInterface {
public $dataTable = 'oauth_auth_codes';
public $ttl = 3600; // 1h
/**
* @inheritdoc
*/
public function get($code) {
$result = (new Key($this->dataTable, $code))->getValue();
if (!$result) {
return null;
}
if ($result['expire_time'] < time()) {
return null;
}
return (new AuthCodeEntity($this->server))->hydrate([
'id' => $result['id'],
'redirectUri' => $result['client_redirect_uri'],
'expireTime' => $result['expire_time'],
'sessionId' => $result['sessionId'],
]);
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $sessionId, $redirectUri) {
$payload = [
'id' => $token,
'expire_time' => $expireTime,
'session_id' => $sessionId,
'client_redirect_uri' => $redirectUri,
];
(new Key($this->dataTable, $token))->setValue($payload)->expire($this->ttl);
}
/**
* @inheritdoc
*/
public function getScopes(OriginalAuthCodeEntity $token) {
$result = (new Set($this->dataTable, $token->getId(), 'scopes'));
$response = [];
foreach ($result as $scope) {
// TODO: нужно проверить все выданные скоупы на их существование
$response[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
return $response;
}
/**
* @inheritdoc
*/
public function associateScope(OriginalAuthCodeEntity $token, ScopeEntity $scope) {
(new Set($this->dataTable, $token->getId(), 'scopes'))->add($scope->getId())->expire($this->ttl);
}
/**
* @inheritdoc
*/
public function delete(OriginalAuthCodeEntity $token) {
// Удаляем ключ
(new Set($this->dataTable, $token->getId()))->delete();
// Удаляем список скоупов для ключа
(new Set($this->dataTable, $token->getId(), 'scopes'))->delete();
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace Fahmiardi\OAuth2\Server\Storage\Redis;
use common\components\redis\Key;
use League\OAuth2\Server\Entity\RefreshTokenEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\RefreshTokenInterface;
class RefreshTokenStorage extends AbstractStorage implements RefreshTokenInterface {
public $dataTable = 'oauth_refresh_tokens';
/**
* @inheritdoc
*/
public function get($token) {
$result = (new Key($this->dataTable, $token))->getValue();
if (!$result) {
return null;
}
return (new RefreshTokenEntity($this->server))
->setId($result['id'])
->setExpireTime($result['expire_time'])
->setAccessTokenId($result['access_token_id']);
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $accessToken) {
$payload = [
'id' => $token,
'expire_time' => $expireTime,
'access_token_id' => $accessToken,
];
(new Key($this->dataTable, $token))->setValue($payload);
}
/**
* @inheritdoc
*/
public function delete(RefreshTokenEntity $token) {
(new Key($this->dataTable, $token->getId()))->delete();
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace common\components\oauth\Storage\Yii2;
use common\components\oauth\Entity\AccessTokenEntity;
use common\models\OauthAccessToken;
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\AccessTokenInterface;
use yii\db\Exception;
class AccessTokenStorage extends AbstractStorage implements AccessTokenInterface {
private $cache = [];
/**
* @param string $token
* @return OauthAccessToken|null
*/
private function getTokenModel($token) {
if (isset($this->cache[$token])) {
$this->cache[$token] = OauthAccessToken::findOne($token);
}
return $this->cache[$token];
}
/**
* @inheritdoc
*/
public function get($token) {
$model = $this->getTokenModel($token);
if ($model === null) {
return null;
}
return (new AccessTokenEntity($this->server))->hydrate([
'id' => $model->access_token,
'expireTime' => $model->expire_time,
'sessionId' => $model->session_id,
]);
}
/**
* @inheritdoc
*/
public function getScopes(OriginalAccessTokenEntity $token) {
$entities = [];
foreach($this->getTokenModel($token->getId())->getScopes() as $scope) {
$entities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
return $entities;
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $sessionId) {
$model = new OauthAccessToken([
'access_token' => $token,
'expire_time' => $expireTime,
'session_id' => $sessionId,
]);
if (!$model->save()) {
throw new Exception('Cannot save ' . OauthAccessToken::class . ' model.');
}
}
/**
* @inheritdoc
*/
public function associateScope(OriginalAccessTokenEntity $token, ScopeEntity $scope) {
$this->getTokenModel($token->getId())->getScopes()->add($scope->getId());
}
/**
* @inheritdoc
*/
public function delete(OriginalAccessTokenEntity $token) {
$this->getTokenModel($token->getId())->delete();
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace common\components\oauth\Storage\Yii2;
use common\components\oauth\Entity\SessionEntity;
use common\models\OauthClient;
use League\OAuth2\Server\Entity\ClientEntity;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\ClientInterface;
class ClientStorage extends AbstractStorage implements ClientInterface {
/**
* @inheritdoc
*/
public function get($clientId, $clientSecret = null, $redirectUri = null, $grantType = null) {
$query = OauthClient::find()
->select(['id', 'name', 'secret'])
->where([OauthClient::tableName() . '.id' => $clientId]);
if ($clientSecret !== null) {
$query->andWhere(['secret' => $clientSecret]);
}
if ($redirectUri !== null) {
$query
->addSelect(['redirect_uri'])
->andWhere(['redirect_uri' => $redirectUri]);
}
$model = $query->asArray()->one();
if ($model === null) {
return null;
}
$entity = new ClientEntity($this->server);
$entity->hydrate([
'id' => $model['id'],
'name' => $model['name'],
'secret' => $model['secret'],
]);
if (isset($model['redirect_uri'])) {
$entity->hydrate([
'redirectUri' => $model['redirect_uri'],
]);
}
return $entity;
}
/**
* @inheritdoc
*/
public function getBySession(OriginalSessionEntity $session) {
if (!$session instanceof SessionEntity) {
throw new \ErrorException('This module assumes that $session typeof ' . SessionEntity::class);
}
$model = OauthClient::find()
->select(['id', 'name'])
->andWhere(['id' => $session->getClientId()])
->asArray()
->one();
if ($model === null) {
return null;
}
return (new ClientEntity($this->server))->hydrate($model);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace common\components\oauth\Storage\Yii2;
use common\models\OauthScope;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\ScopeInterface;
class ScopeStorage extends AbstractStorage implements ScopeInterface {
/**
* @inheritdoc
*/
public function get($scope, $grantType = null, $clientId = null) {
$row = OauthScope::find()->andWhere(['id' => $scope])->asArray()->one();
if ($row === null) {
return null;
}
$entity = new ScopeEntity($this->server);
$entity->hydrate($row);
return $entity;
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace common\components\oauth\Storage\Yii2;
use common\components\oauth\Entity\AuthCodeEntity;
use common\components\oauth\Entity\SessionEntity;
use common\models\OauthSession;
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\SessionInterface;
use yii\db\ActiveQuery;
use yii\db\Exception;
class SessionStorage extends AbstractStorage implements SessionInterface {
private $cache = [];
/**
* @param string $sessionId
* @return OauthSession|null
*/
private function getSessionModel($sessionId) {
if (!isset($this->cache[$sessionId])) {
$this->cache[$sessionId] = OauthSession::findOne($sessionId);
}
return $this->cache[$sessionId];
}
private function hydrateEntity($sessionModel) {
if (!$sessionModel instanceof OauthSession) {
return null;
}
return (new SessionEntity($this->server))->hydrate([
'id' => $sessionModel->id,
'client_id' => $sessionModel->client_id,
])->setOwner($sessionModel->owner_type, $sessionModel->owner_id);
}
/**
* @param string $sessionId
* @return SessionEntity|null
*/
public function getSession($sessionId) {
return $this->hydrateEntity($this->getSessionModel($sessionId));
}
/**
* @inheritdoc
*/
public function getByAccessToken(OriginalAccessTokenEntity $accessToken) {
/** @var OauthSession|null $model */
$model = OauthSession::find()->innerJoinWith([
'accessTokens' => function(ActiveQuery $query) use ($accessToken) {
$query->andWhere(['access_token' => $accessToken->getId()]);
},
])->one();
return $this->hydrateEntity($model);
}
/**
* @inheritdoc
*/
public function getByAuthCode(OriginalAuthCodeEntity $authCode) {
if (!$authCode instanceof AuthCodeEntity) {
throw new \ErrorException('This module assumes that $authCode typeof ' . AuthCodeEntity::class);
}
return $this->getSession($authCode->getSessionId());
}
/**
* {@inheritdoc}
*/
public function getScopes(OriginalSessionEntity $session) {
$result = [];
foreach ($this->getSessionModel($session->getId())->getScopes() as $scope) {
// TODO: нужно проверить все выданные скоупы на их существование
$result[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
return $result;
}
/**
* @inheritdoc
*/
public function create($ownerType, $ownerId, $clientId, $clientRedirectUri = null) {
$sessionId = OauthSession::find()
->select('id')
->andWhere([
'client_id' => $clientId,
'owner_type' => $ownerType,
'owner_id' => $ownerId,
])->scalar();
if ($sessionId === false) {
$model = new OauthSession([
'client_id' => $clientId,
'owner_type' => $ownerType,
'owner_id' => $ownerId,
]);
if (!$model->save()) {
throw new Exception('Cannot save ' . OauthSession::class . ' model.');
}
$sessionId = $model->id;
}
return $sessionId;
}
/**
* @inheritdoc
*/
public function associateScope(OriginalSessionEntity $session, ScopeEntity $scope) {
$this->getSessionModel($session->getId())->getScopes()->add($scope->getId());
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace common\components\redis;
use InvalidArgumentException;
use Yii;
class Key {
protected $key;
/**
* @return \yii\redis\Connection
*/
public function getRedis() {
return Yii::$app->get('redis');
}
public function getKey() {
return $this->key;
}
public function getValue() {
return $this->getRedis()->get(json_decode($this->key));
}
public function setValue($value) {
$this->getRedis()->set($this->key, json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
return $this;
}
public function delete() {
$this->getRedis()->executeCommand('DEL', [$this->key]);
return $this;
}
public function expire($ttl) {
$this->getRedis()->executeCommand('EXPIRE', [$this->key, $ttl]);
return $this;
}
private function buildKey(array $parts) {
$keyParts = [];
foreach($parts as $part) {
$keyParts[] = str_replace('_', ':', $part);
}
return implode(':', $keyParts);
}
public function __construct(...$key) {
if (empty($key)) {
throw new InvalidArgumentException('You must specify at least one key.');
}
$this->key = $this->buildKey($key);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace common\components\redis;
use IteratorAggregate;
use Yii;
class Set extends Key implements IteratorAggregate {
/**
* @return \yii\redis\Connection
*/
public static function getDb() {
return Yii::$app->get('redis');
}
public function add($value) {
$this->getDb()->executeCommand('SADD', [$this->key, $value]);
return $this;
}
public function remove($value) {
$this->getDb()->executeCommand('SREM', [$this->key, $value]);
return $this;
}
public function members() {
return $this->getDb()->executeCommand('SMEMBERS', [$this->key]);
}
public function getValue() {
return $this->members();
}
public function exists($value) {
return !!$this->getDb()->executeCommand('SISMEMBER', [$this->key, $value]);
}
public function diff(array $sets) {
return $this->getDb()->executeCommand('SDIFF', [$this->key, implode(' ', $sets)]);
}
/**
* @inheritdoc
*/
public function getIterator() {
return new \ArrayIterator($this->members());
}
}

View File

@ -15,6 +15,9 @@ return [
],
'security' => [
'passwordHashStrategy' => 'password_hash',
]
],
'redis' => [
'class' => 'yii\redis\Connection',
],
],
];

View File

@ -28,6 +28,7 @@ use yii\web\IdentityInterface;
*
* Отношения:
* @property EmailActivation[] $emailActivations
* @property OauthSession[] $sessions
*
* Поведения:
* @mixin TimestampBehavior
@ -216,4 +217,34 @@ class Account extends ActiveRecord implements IdentityInterface {
return $this->hasMany(EmailActivation::class, ['id' => 'account_id']);
}
public function getSessions() {
return $this->hasMany(OauthSession::class, ['owner_id' => 'id']);
}
/**
* Метод проверяет, может ли текщий пользователь быть автоматически авторизован
* для указанного клиента без запроса доступа к необходимому списку прав
*
* @param OauthClient $client
* @param \League\OAuth2\Server\Entity\ScopeEntity[] $scopes
*
* @return bool
*/
public function canAutoApprove(OauthClient $client, array $scopes = []) {
if ($client->is_trusted) {
return true;
}
/** @var OauthSession|null $session */
$session = $this->getSessions()->andWhere(['client_id' => $client->id])->one();
if ($session !== null) {
$existScopes = $session->getScopes()->members();
if (empty(array_diff(array_keys($scopes), $existScopes))) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace common\models;
use common\components\redis\Set;
use Yii;
use yii\db\ActiveRecord;
/**
* This is the model class for table "oauth_access_tokens".
*
* @property string $access_token
* @property string $session_id
* @property integer $expire_time
*
* @property Set $scopes
*/
class OauthAccessToken extends ActiveRecord {
public static function tableName() {
return '{{%oauth_access_tokens}}';
}
public function getSession() {
return $this->hasOne(OauthSession::class, ['id' => 'session_id']);
}
public function getScopes() {
return new Set($this->getDb()->getSchema()->getRawTableName($this->tableName()), $this->access_token, 'scopes');
}
public function beforeDelete() {
if (!$result = parent::beforeDelete()) {
return $result;
}
$this->getScopes()->delete();
return true;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace common\models;
use Yii;
use yii\db\ActiveRecord;
/**
* Поля модели:
* @property string $id
* @property string $secret
* @property string $name
* @property string $description
* @property string $redirect_uri
* @property integer $account_id
* @property bool $is_trusted
* @property integer $created_at
*
* Отношения:
* @property Account $account
* @property OauthSession[] $sessions
*/
class OauthClient extends ActiveRecord {
public static function tableName() {
return '{{%oauth_clients}}';
}
public function rules() {
return [
[['id'], 'required', 'when' => function(self $model) {
return $model->isNewRecord;
}],
[['id'], 'unique', 'when' => function(self $model) {
return $model->isNewRecord;
}],
[['name', 'description'], 'required'],
[['name', 'description'], 'string', 'max' => 255],
];
}
public function getAccount() {
return $this->hasOne(Account::class, ['id' => 'account_id']);
}
public function getSessions() {
return $this->hasMany(OauthSession::class, ['client_id' => 'id']);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace common\models;
use Yii;
use yii\db\ActiveRecord;
/**
* Поля:
* @property string $id
*/
class OauthScope extends ActiveRecord {
public static function tableName() {
return '{{%oauth_scopes}}';
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace common\models;
use common\components\redis\Set;
use Yii;
use yii\db\ActiveRecord;
/**
* Поля:
* @property integer $id
* @property string $owner_type
* @property string $owner_id
* @property string $client_id
* @property string $client_redirect_uri
*
* Отношения
* @property OauthAccessToken[] $accessTokens
* @property OauthClient $client
* @property Account $account
* @property Set $scopes
*/
class OauthSession extends ActiveRecord {
public static function tableName() {
return '{{%oauth_sessions}}';
}
public function getOauthAccessTokens() {
return $this->hasMany(OauthAccessToken::class, ['session_id' => 'id']);
}
public function getClient() {
return $this->hasOne(OauthClient::class, ['id' => 'client_id']);
}
public function getAccount() {
return $this->hasOne(Account::class, ['id' => 'owner_id']);
}
public function getScopes() {
return new Set($this->getDb()->getSchema()->getRawTableName($this->tableName()), $this->id, 'scopes');
}
public function beforeDelete() {
if (!$result = parent::beforeDelete()) {
return $result;
}
$this->getScopes()->delete();
return true;
}
}

View File

@ -18,14 +18,19 @@
"yiisoft/yii2": ">=2.0.6",
"yiisoft/yii2-bootstrap": "*",
"yiisoft/yii2-swiftmailer": "*",
"ramsey/uuid": "^3.1"
"ramsey/uuid": "^3.1",
"league/oauth2-server": "~4.1.5",
"yiisoft/yii2-redis": "~2.0.0"
},
"require-dev": {
"yiisoft/yii2-codeception": "*",
"yiisoft/yii2-debug": "*",
"yiisoft/yii2-gii": "*",
"yiisoft/yii2-faker": "*",
"flow/jsonpath": "^0.3.1"
"flow/jsonpath": "^0.3.1",
"codeception/codeception": "2.0.*",
"codeception/specify": "*",
"codeception/verify": "*"
},
"config": {
"process-timeout": 1800

View File

@ -11,11 +11,34 @@ class Migration extends YiiMigration {
public function getTableOptions($engine = 'InnoDB') {
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
// http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=' . $engine;
}
return $tableOptions;
}
protected function primary(...$columns) {
switch (count($columns)) {
case 0:
$key = '';
break;
case 1:
$key = $columns[0];
break;
default:
$key = $this->buildKey($columns);
}
return " PRIMARY KEY ($key) ";
}
private function buildKey(array $columns) {
$key = '';
foreach ($columns as $i => $column) {
$key .= $i == count($columns) ? $column : "$column,";
}
return $key;
}
}

View File

@ -0,0 +1,77 @@
<?php
use console\db\Migration;
class m160201_055928_oauth extends Migration {
public function safeUp() {
$this->createTable('{{%oauth_clients}}', [
'id' => $this->string(64),
'secret' => $this->string()->notNull(),
'name' => $this->string()->notNull(),
'description' => $this->string(),
'redirect_uri' => $this->string()->notNull(),
'account_id' => $this->getDb()->getTableSchema('{{%accounts}}')->getColumn('id')->dbType,
'is_trusted' => $this->boolean()->defaultValue(false)->notNull(),
'created_at' => $this->integer()->notNull(),
$this->primary('id'),
], $this->tableOptions);
$this->createTable('{{%oauth_scopes}}', [
'id' => $this->string(64),
$this->primary('id'),
], $this->tableOptions);
$this->createTable('{{%oauth_sessions}}', [
'id' => $this->primaryKey(),
'owner_type' => $this->string()->notNull(),
'owner_id' => $this->string()->notNull(),
'client_id' => $this->getDb()->getTableSchema('{{%oauth_clients}}')->getColumn('id')->dbType,
'client_redirect_uri' => $this->string(),
], $this->tableOptions);
$this->createTable('{{%oauth_access_tokens}}', [
'access_token' => $this->string(64),
'session_id' => $this->getDb()->getTableSchema('{{%oauth_sessions}}')->getColumn('id')->dbType,
'expire_time' => $this->integer()->notNull(),
$this->primary('access_token'),
], $this->tableOptions);
$this->addForeignKey(
'FK_oauth_client_to_accounts',
'{{%oauth_clients}}',
'account_id',
'{{%accounts}}',
'id',
'CASCADE'
);
$this->addForeignKey(
'FK_oauth_session_to_client',
'{{%oauth_sessions}}',
'client_id',
'{{%oauth_clients}}',
'id',
'CASCADE',
'CASCADE'
);
$this->addForeignKey(
'FK_oauth_access_toke_to_oauth_session',
'{{%oauth_access_tokens}}',
'session_id',
'{{%oauth_sessions}}',
'id',
'CASCADE',
'SET NULL'
);
}
public function safeDown() {
$this->dropTable('{{%oauth_access_tokens}}');
$this->dropTable('{{%oauth_sessions}}');
$this->dropTable('{{%oauth_scopes}}');
$this->dropTable('{{%oauth_clients}}');
}
}

View File

@ -6,6 +6,9 @@ $config = [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
'reCaptcha' => [
'secret' => '',
],
],
];

View File

@ -12,5 +12,11 @@ return [
// for the mailer to send real emails.
'useFileTransport' => true,
],
'redis' => [
'hostname' => 'localhost',
'password' => null,
'port' => 6379,
'database' => 0,
],
],
];

View File

@ -0,0 +1,12 @@
<?php
return [
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
'reCaptcha' => [
'secret' => '',
],
],
];

View File

@ -0,0 +1,3 @@
<?php
return [
];

View File

@ -0,0 +1,18 @@
<?php
defined('YII_DEBUG') or define('YII_DEBUG', false);
defined('YII_ENV') or define('YII_ENV', 'prod');
require(__DIR__ . '/../../vendor/autoload.php');
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
require(__DIR__ . '/../../common/config/bootstrap.php');
require(__DIR__ . '/../config/bootstrap.php');
$config = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../../common/config/main.php'),
require(__DIR__ . '/../../common/config/main-local.php'),
require(__DIR__ . '/../config/main.php'),
require(__DIR__ . '/../config/main-local.php')
);
$application = new yii\web\Application($config);
$application->run();

View File

@ -6,5 +6,11 @@ return [
'username' => 'root',
'password' => '',
],
'redis' => [
'hostname' => 'localhost',
'password' => null,
'port' => 6379,
'database' => 0,
],
],
];

View File

@ -21,3 +21,6 @@ $_SERVER['SERVER_NAME'] = parse_url(\Codeception\Configuration::config()['confi
$_SERVER['SERVER_PORT'] = parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_PORT) ?: '80';
Yii::setAlias('@tests', dirname(dirname(__DIR__)));
// disable deep cloning of properties inside specify block
\Codeception\Specify\Config::setDeepClone(false);

View File

@ -0,0 +1,21 @@
<?php
namespace tests\codeception\api\_pages;
use yii\codeception\BasePage;
/**
* @property \tests\codeception\api\FunctionalTester $actor
*/
class OauthRoute extends BasePage {
public function validate($queryParams) {
$this->route = ['oauth/validate'];
$this->actor->sendGET($this->getUrl($queryParams));
}
public function complete($queryParams = [], $postParams = []) {
$this->route = ['oauth/complete'];
$this->actor->sendPOST($this->getUrl($queryParams), $postParams);
}
}

View File

@ -13,6 +13,11 @@ modules:
- Yii2
- tests\codeception\common\_support\FixtureHelper
- REST
- Redis
config:
Yii2:
configFile: '../config/api/functional.php'
Redis:
host: localhost
port: 6379
database: 1

View File

@ -1,47 +0,0 @@
<?php
use tests\codeception\api\FunctionalTester;
use tests\codeception\api\_pages\ContactPage;
/* @var $scenario Codeception\Scenario */
$I = new FunctionalTester($scenario);
$I->wantTo('ensure that contact works');
$contactPage = ContactPage::openBy($I);
$I->see('Contact', 'h1');
$I->amGoingTo('submit contact form with no data');
$contactPage->submit([]);
$I->expectTo('see validations errors');
$I->see('Contact', 'h1');
$I->see('Name cannot be blank', '.help-block');
$I->see('Email cannot be blank', '.help-block');
$I->see('Subject cannot be blank', '.help-block');
$I->see('Body cannot be blank', '.help-block');
$I->see('The verification code is incorrect', '.help-block');
$I->amGoingTo('submit contact form with not correct email');
$contactPage->submit([
'name' => 'tester',
'email' => 'tester.email',
'subject' => 'test subject',
'body' => 'test content',
'verifyCode' => 'testme',
]);
$I->expectTo('see that email adress is wrong');
$I->dontSee('Name cannot be blank', '.help-block');
$I->see('Email is not a valid email address.', '.help-block');
$I->dontSee('Subject cannot be blank', '.help-block');
$I->dontSee('Body cannot be blank', '.help-block');
$I->dontSee('The verification code is incorrect', '.help-block');
$I->amGoingTo('submit contact form with correct data');
$contactPage->submit([
'name' => 'tester',
'email' => 'tester@example.com',
'subject' => 'test subject',
'body' => 'test content',
'verifyCode' => 'testme',
]);
$I->see('Thank you for contacting us. We will respond to you as soon as possible.');

View File

@ -0,0 +1,294 @@
<?php
namespace tests\codeception\api;
use tests\codeception\api\_pages\OauthRoute;
use tests\codeception\api\functional\_steps\AccountSteps;
class OauthCest {
/**
* @var OauthRoute
*/
private $route;
public function _before(FunctionalTester $I) {
$this->route = new OauthRoute($I);
}
public function testValidateRequest(FunctionalTester $I) {
$this->testOauthParamsValidation($I, 'validate');
$I->wantTo('validate and obtain information about new auth request');
$this->route->validate($this->buildQueryParams(
'ely',
'http://ely.by',
'code',
[
'minecraft_server_session'
],
'test-state'
));
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'success' => true,
'oAuth' => [
'client_id' => 'ely',
'redirect_uri' => 'http://ely.by',
'response_type' => 'code',
'scope' => 'minecraft_server_session',
'state' => 'test-state',
],
'client' => [
'id' => 'ely',
'name' => 'Ely.by',
'description' => 'Всем знакомое елуби',
],
'session' => [
'scopes' => [
'minecraft_server_session',
],
],
]);
}
public function testValidateWithDescriptionReplaceRequest(FunctionalTester $I) {
$I->wantTo('validate and get information with description replacement');
$this->route->validate($this->buildQueryParams(
'ely',
'http://ely.by',
'code',
null,
null,
[
'description' => 'all familiar eliby',
]
));
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'client' => [
'description' => 'all familiar eliby',
],
]);
}
public function testCompleteValidationAction($I, $scenario) {
$I = new AccountSteps($scenario);
$I->loggedInAsActiveAccount();
$I->wantTo('validate all oAuth params on complete request');
$this->testOauthParamsValidation($I, 'complete');
}
public function testCompleteActionOnWrongConditions($I, $scenario) {
$I = new AccountSteps($scenario);
$I->loggedInAsActiveAccount();
$I->wantTo('get accept_required if I dom\'t require any scope, but this is first time request');
$I->cleanupRedis();
$this->route->complete($this->buildQueryParams(
'ely',
'http://ely.by',
'code'
));
$I->canSeeResponseCodeIs(401);
$I->canSeeResponseContainsJson([
'success' => false,
'error' => 'accept_required',
'parameter' => '',
'statusCode' => 401,
]);
$I->wantTo('get accept_required if I require some scopes on first time');
$this->route->complete($this->buildQueryParams(
'ely',
'http://ely.by',
'code',
['minecraft_server_session']
));
$I->canSeeResponseCodeIs(401);
$I->canSeeResponseContainsJson([
'success' => false,
'error' => 'accept_required',
'parameter' => '',
'statusCode' => 401,
]);
}
public function testCompleteActionSuccess($I, $scenario) {
$I = new AccountSteps($scenario);
$I->loggedInAsActiveAccount();
$I->wantTo('get auth code if I require some scope and pass accept field');
$this->route->complete($this->buildQueryParams(
'ely',
'http://ely.by',
'code',
['minecraft_server_session']
), ['accept' => true]);
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseContainsJson([
'success' => true,
]);
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
$I->wantTo('get auth code if I don\'t require any scope and don\'t pass accept field, but previously have ' .
'successful request');
$this->route->complete($this->buildQueryParams(
'ely',
'http://ely.by',
'code'
));
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseContainsJson([
'success' => true,
]);
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
$I->wantTo('get auth code if I require some scopes and don\'t pass accept field, but previously have successful ' .
'request with same scopes');
$this->route->complete($this->buildQueryParams(
'ely',
'http://ely.by',
'code',
['minecraft_server_session']
));
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseContainsJson([
'success' => true,
]);
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
}
public function testAcceptRequiredOnNewScope($I, $scenario) {
$I = new AccountSteps($scenario);
$I->loggedInAsActiveAccount();
$I->wantTo('get accept_required if I have previous successful request, but now require some new scope');
$this->route->complete($this->buildQueryParams(
'ely',
'http://ely.by',
'code',
['minecraft_server_session']
), ['accept' => true]);
$this->route->complete($this->buildQueryParams(
'ely',
'http://ely.by',
'code',
['minecraft_server_session', 'change_skin']
));
$I->canSeeResponseCodeIs(401);
$I->canSeeResponseContainsJson([
'success' => false,
'error' => 'accept_required',
'parameter' => '',
'statusCode' => 401,
]);
}
public function testCompleteActionWithDismissState($I, $scenario) {
$I = new AccountSteps($scenario);
$I->loggedInAsActiveAccount();
$I->wantTo('get access_denied error if I pass accept in false state');
$this->route->complete($this->buildQueryParams(
'ely',
'http://ely.by',
'code',
['minecraft_server_session']
), ['accept' => false]);
$I->canSeeResponseCodeIs(401);
$I->canSeeResponseContainsJson([
'success' => false,
'error' => 'access_denied',
'parameter' => '',
'statusCode' => 401,
]);
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
}
private function buildQueryParams(
$clientId = null,
$redirectUri = null,
$responseType = null,
$scopes = [],
$state = null,
$customData = []
) {
$params = $customData;
if ($clientId !== null) {
$params['client_id'] = $clientId;
}
if ($redirectUri !== null) {
$params['redirect_uri'] = $redirectUri;
}
if ($responseType !== null) {
$params['response_type'] = $responseType;
}
if ($state !== null) {
$params['state'] = $state;
}
if (!empty($scopes)) {
if (is_array($scopes)) {
$scopes = implode(',', $scopes);
}
$params['scope'] = $scopes;
}
return $params;
}
private function testOauthParamsValidation(FunctionalTester $I, $action) {
$I->wantTo('check behavior on invalid request without one or few params');
$this->route->$action($this->buildQueryParams());
$I->canSeeResponseCodeIs(400);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'success' => false,
'error' => 'invalid_request',
'parameter' => 'client_id',
'statusCode' => 400,
]);
$I->wantTo('check behavior on invalid client id');
$this->route->$action($this->buildQueryParams('non-exists-client', 'http://some-resource.by', 'code'));
$I->canSeeResponseCodeIs(401);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'success' => false,
'error' => 'invalid_client',
'statusCode' => 401,
]);
$I->wantTo('check behavior on invalid response type');
$this->route->$action($this->buildQueryParams('ely', 'http://ely.by', 'kitty'));
$I->canSeeResponseCodeIs(400);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'success' => false,
'error' => 'unsupported_response_type',
'parameter' => 'kitty',
'statusCode' => 400,
]);
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
$I->wantTo('check behavior on some invalid scopes');
$this->route->$action($this->buildQueryParams('ely', 'http://ely.by', 'code', [
'minecraft_server_session',
'some_wrong_scope',
]));
$I->canSeeResponseCodeIs(400);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'success' => false,
'error' => 'invalid_scope',
'parameter' => 'some_wrong_scope',
'statusCode' => 400,
]);
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
}
}

View File

@ -1,3 +1,5 @@
<?php
new yii\web\Application(require(dirname(dirname(__DIR__)) . '/config/api/functional.php'));
\Codeception\Util\Autoload::registerSuffix('Steps', __DIR__ . DIRECTORY_SEPARATOR);

View File

@ -0,0 +1,16 @@
<?php
namespace tests\codeception\api\functional\_steps;
use tests\codeception\api\_pages\LoginRoute;
use tests\codeception\api\FunctionalTester;
class AccountSteps extends FunctionalTester {
public function loggedInAsActiveAccount() {
$I = $this;
$route = new LoginRoute($I);
$route->login('Admin', 'password_0');
$I->canSeeResponseIsJson();
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace tests\codeception\api\unit\models;
use Yii;
use tests\codeception\api\unit\TestCase;
use api\models\ContactForm;
class ContactFormTest extends TestCase
{
use \Codeception\Specify;
protected function setUp()
{
parent::setUp();
Yii::$app->mailer->fileTransportCallback = function ($mailer, $message) {
return 'testing_message.eml';
};
}
protected function tearDown()
{
unlink($this->getMessageFile());
parent::tearDown();
}
public function testContact()
{
$model = new ContactForm();
$model->attributes = [
'name' => 'Tester',
'email' => 'tester@example.com',
'subject' => 'very important letter subject',
'body' => 'body of current message',
];
$model->sendEmail('admin@example.com');
$this->specify('email should be send', function () {
expect('email file should exist', file_exists($this->getMessageFile()))->true();
});
$this->specify('message should contain correct data', function () use ($model) {
$emailMessage = file_get_contents($this->getMessageFile());
expect('email should contain user name', $emailMessage)->contains($model->name);
expect('email should contain sender email', $emailMessage)->contains($model->email);
expect('email should contain subject', $emailMessage)->contains($model->subject);
expect('email should contain body', $emailMessage)->contains($model->body);
});
}
private function getMessageFile()
{
return Yii::getAlias(Yii::$app->mailer->fileTransportPath) . '/testing_message.eml';
}
}

View File

@ -1,87 +0,0 @@
<?php
namespace tests\codeception\api\models;
use Yii;
use tests\codeception\api\unit\DbTestCase;
use api\models\PasswordResetRequestForm;
use tests\codeception\common\fixtures\UserFixture;
use common\models\Account;
use Codeception\Specify;
class PasswordResetRequestFormTest extends DbTestCase
{
use Specify;
protected function setUp()
{
parent::setUp();
Yii::$app->mailer->fileTransportCallback = function ($mailer, $message) {
return 'testing_message.eml';
};
}
protected function tearDown()
{
@unlink($this->getMessageFile());
parent::tearDown();
}
public function testSendEmailWrongUser()
{
$this->specify('no user with such email, message should not be sent', function () {
$model = new PasswordResetRequestForm();
$model->email = 'not-existing-email@example.com';
expect('email not sent', $model->sendEmail())->false();
});
$this->specify('user is not active, message should not be sent', function () {
$model = new PasswordResetRequestForm();
$model->email = $this->user[1]['email'];
expect('email not sent', $model->sendEmail())->false();
});
}
public function testSendEmailCorrectUser()
{
$model = new PasswordResetRequestForm();
$model->email = $this->user[0]['email'];
$user = Account::findOne(['password_reset_token' => $this->user[0]['password_reset_token']]);
expect('email sent', $model->sendEmail())->true();
expect('user has valid token', $user->password_reset_token)->notNull();
$this->specify('message has correct format', function () use ($model) {
expect('message file exists', file_exists($this->getMessageFile()))->true();
$message = file_get_contents($this->getMessageFile());
expect('message "from" is correct', $message)->contains(Yii::$app->params['supportEmail']);
expect('message "to" is correct', $message)->contains($model->email);
});
}
public function fixtures()
{
return [
'user' => [
'class' => UserFixture::className(),
'dataFile' => '@tests/codeception/api/unit/fixtures/data/models/user.php'
],
];
}
private function getMessageFile()
{
return Yii::getAlias(Yii::$app->mailer->fileTransportPath) . '/testing_message.eml';
}
}

View File

@ -1,43 +0,0 @@
<?php
namespace tests\codeception\api\unit\models;
use tests\codeception\api\unit\DbTestCase;
use tests\codeception\common\fixtures\UserFixture;
use api\models\ResetPasswordForm;
class ResetPasswordFormTest extends DbTestCase
{
/**
* @expectedException \yii\base\InvalidParamException
*/
public function testResetWrongToken()
{
new ResetPasswordForm('notexistingtoken_1391882543');
}
/**
* @expectedException \yii\base\InvalidParamException
*/
public function testResetEmptyToken()
{
new ResetPasswordForm('');
}
public function testResetCorrectToken()
{
$form = new ResetPasswordForm($this->user[0]['password_reset_token']);
expect('password should be resetted', $form->resetPassword())->true();
}
public function fixtures()
{
return [
'user' => [
'class' => UserFixture::className(),
'dataFile' => '@tests/codeception/api/unit/fixtures/data/models/user.php'
],
];
}
}

View File

@ -4,6 +4,9 @@ namespace tests\codeception\common\_support;
use Codeception\Module;
use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\fixtures\EmailActivationFixture;
use tests\codeception\common\fixtures\OauthClientFixture;
use tests\codeception\common\fixtures\OauthScopeFixture;
use tests\codeception\common\fixtures\OauthSessionFixture;
use yii\test\FixtureTrait;
use yii\test\InitDbFixture;
@ -26,35 +29,20 @@ class FixtureHelper extends Module {
getFixture as protected;
}
/**
* Method called before any suite tests run. Loads User fixture login user
* to use in functional tests.
*
* @param array $settings
*/
public function _beforeSuite($settings = []) {
$this->loadFixtures();
}
/**
* Method is called after all suite tests run
*/
public function _afterSuite() {
$this->unloadFixtures();
}
/**
* @inheritdoc
*/
public function globalFixtures() {
return [
InitDbFixture::className(),
];
}
/**
* @inheritdoc
*/
public function fixtures() {
return [
'accounts' => [
@ -65,6 +53,19 @@ class FixtureHelper extends Module {
'class' => EmailActivationFixture::class,
'dataFile' => '@tests/codeception/common/fixtures/data/email-activations.php',
],
'oauthClients' => [
'class' => OauthClientFixture::class,
'dataFile' => '@tests/codeception/common/fixtures/data/oauth-clients.php',
],
'oauthScopes' => [
'class' => OauthScopeFixture::class,
'dataFile' => '@tests/codeception/common/fixtures/data/oauth-scopes.php',
],
'oauthSessions' => [
'class' => OauthSessionFixture::class,
'dataFile' => '@tests/codeception/common/fixtures/data/oauth-sessions.php',
],
];
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace tests\codeception\common\fixtures;
use common\models\OauthClient;
use yii\test\ActiveFixture;
class OauthClientFixture extends ActiveFixture {
public $modelClass = OauthClient::class;
public $depends = [
AccountFixture::class,
];
}

View File

@ -0,0 +1,11 @@
<?php
namespace tests\codeception\common\fixtures;
use common\models\OauthScope;
use yii\test\ActiveFixture;
class OauthScopeFixture extends ActiveFixture {
public $modelClass = OauthScope::class;
}

View File

@ -0,0 +1,17 @@
<?php
namespace tests\codeception\common\fixtures;
use common\models\OauthScope;
use common\models\OauthSession;
use yii\test\ActiveFixture;
class OauthSessionFixture extends ActiveFixture {
public $modelClass = OauthSession::class;
public $depends = [
OauthClientFixture::class,
AccountFixture::class,
];
}

View File

@ -0,0 +1,23 @@
<?php
return [
'ely' => [
'id' => 'ely',
'secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
'name' => 'Ely.by',
'description' => 'Всем знакомое елуби',
'redirect_uri' => 'http://ely.by',
'account_id' => NULL,
'is_trusted' => 0,
'created_at' => 1455309271,
],
'tlauncher' => [
'id' => 'tlauncher',
'secret' => 'HsX-xXzdGiz3mcsqeEvrKHF47sqiaX94',
'name' => 'TLauncher',
'description' => 'Лучший альтернативный лаунчер для Minecraft с большим количеством версий и их модификаций, а также возмоностью входа как с лицензионным аккаунтом, так и без него.',
'redirect_uri' => '',
'account_id' => NULL,
'is_trusted' => 0,
'created_at' => 1455318468,
],
];

View File

@ -0,0 +1,9 @@
<?php
return [
'minecraft_server_session' => [
'id' => 'minecraft_server_session',
],
'change_skin' => [
'id' => 'change_skin',
],
];

View File

@ -0,0 +1,3 @@
<?php
return [
];

View File

@ -22,5 +22,8 @@ return [
'urlManager' => [
'showScriptName' => true,
],
'redis' => [
'database' => 1,
],
],
];