Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 112 additions & 36 deletions Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -1245,10 +1245,7 @@ function doRegister() {
$cpf = str_replace(".","",$cpf); // remove "."

// generate the token hash
$source = rand(3333, 8888);
$cut = rand(10, 30);
$string = $this->hashPassword($source);
$token = substr($string, $cut, 20);
$token = $this->generateAccountValidationToken();

// Oauth pattern
$response = [
Expand All @@ -1273,8 +1270,6 @@ function doRegister() {
$user = $this->_createUser($response);
$app->applyHookBoundTo($this, 'auth.createUser:after', [$user, $response]);

$baseUrl = $app->getBaseUrl();

if(!$user) {
$error['user']['createUser'] = i::__('Não foi possível criar o usuário. Entre em contato com suporte', 'multipleLocal');

Expand All @@ -1284,34 +1279,7 @@ function doRegister() {
];
}

//ATENÇÃO !! Se for necessario "padronizar" os emails com header/footers, é necessario adapatar o 'mustache', e criar uma mini estrutura de pasta de emails em 'MultipleLocalAuth\views'
$mustache = new \Mustache_Engine();
$site_name = $app->siteName;
$content = $mustache->render(
file_get_contents(
__DIR__.
DIRECTORY_SEPARATOR.'views'.
DIRECTORY_SEPARATOR.'auth'.
DIRECTORY_SEPARATOR.'email-to-validate-account.html'
), array(
"siteName" => $site_name,
"user" => $user->profile->name,
"urlToValidateAccount" => $baseUrl.'auth/confirma-email?token='.$token,
"baseUrl" => $baseUrl,
"urlSupportChat" => $this->_config['urlSupportChat'],
"urlSupportEmail" => $this->_config['urlSupportEmail'],
"urlSupportSite" => $this->_config['urlSupportSite'],
"textSupportSite" => $this->_config['textSupportSite'],
"urlImageToUseInEmails" => $this->getImageImageURl(),
)
);

$app->createAndSendMailMessage([
'from' => $app->config['mailer.from'],
'to' => $user->email,
'subject' => "Bem-vindo ao ".$site_name,
'body' => $content
]);
$this->sendAccountValidationEmail($user, $token);

$app->disableAccessControl();
$user->{self::$passMetaName} = $app->auth->hashPassword($pass);
Expand Down Expand Up @@ -1356,8 +1324,116 @@ function getImageImageURl () {
return $app->view->asset('img/mail-image.png', false);
}
}



/**
* Gera o token usado no link de validação de conta
*
* @return string
*/
protected function generateAccountValidationToken() {
$source = rand(3333, 8888);
$cut = rand(10, 30);
$string = $this->hashPassword($source);

return substr($string, $cut, 20);
}

/**
* Monta e envia ao usuário o e-mail com o link de validação de conta
*
* @param Entities\User $user usuário destinatário
* @param string $token token de validação
* @return bool true se o e-mail foi enviado
*/
protected function sendAccountValidationEmail(Entities\User $user, $token) {
$app = App::i();

$baseUrl = $app->getBaseUrl();
$site_name = $app->siteName;

//ATENÇÃO !! Se for necessario "padronizar" os emails com header/footers, é necessario adapatar o 'mustache', e criar uma mini estrutura de pasta de emails em 'MultipleLocalAuth\views'
$mustache = new \Mustache_Engine();
$content = $mustache->render(
file_get_contents(
__DIR__.
DIRECTORY_SEPARATOR.'views'.
DIRECTORY_SEPARATOR.'auth'.
DIRECTORY_SEPARATOR.'email-to-validate-account.html'
), array(
"siteName" => $site_name,
"user" => $user->profile->name,
"urlToValidateAccount" => $baseUrl.'auth/confirma-email?token='.$token,
"baseUrl" => $baseUrl,
"urlSupportChat" => $this->_config['urlSupportChat'],
"urlSupportEmail" => $this->_config['urlSupportEmail'],
"urlSupportSite" => $this->_config['urlSupportSite'],
"textSupportSite" => $this->_config['textSupportSite'],
"urlImageToUseInEmails" => $this->getImageImageURl(),
)
);

return (bool) $app->createAndSendMailMessage([
'from' => $app->config['mailer.from'],
'to' => $user->email,
'subject' => "Bem-vindo ao ".$site_name,
'body' => $content
]);
}

/**
* Este provedor valida a conta por e-mail quando a confirmação é exigida
*
* @return bool
*/
function supportsAccountValidation() {
return (bool) ($this->_config['userMustConfirmEmailToUseTheSystem'] ?? false);
}

/**
* A conta é considerada validada exceto quando o metadado accountIsActive é '0'
*
* A comparação reproduz a regra aplicada no login (veja doLogin): usuários sem o
* metadado, anteriores à validação por e-mail, não são bloqueados e portanto não
* estão pendentes de validação.
*
* @param Entities\User $user
* @return bool
*/
function isAccountValidated(Entities\User $user) {
return $user->getMetadata(self::$accountIsActiveMetadata) !== '0';
}

/**
* Reenvia ao usuário o e-mail com o link de validação de conta
*
* Reaproveita o token existente para não invalidar links já enviados e gera um
* novo apenas quando o usuário ainda não tem token.
*
* @param Entities\User $user
* @return bool true se o e-mail foi enviado
*/
function resendAccountValidationEmail(Entities\User $user) {
$app = App::i();

if (!$user->email) {
return false;
}

$token = $user->getMetadata(self::$tokenVerifyAccountMetadata);

if (!$token) {
$token = $this->generateAccountValidationToken();

$app->disableAccessControl();
$user->setMetadata(self::$tokenVerifyAccountMetadata, $token);
$user->saveMetadata(true);
$app->enableAccessControl();
}

return $this->sendAccountValidationEmail($user, $token);
}


/********************************************************************************/
/***************************** OPAUTH METHODS **********************************/
/********************************************************************************/
Expand Down
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,40 @@ Você pode adicionar ou remover estratégias conforme necessário; qualquer estr
- `GET|POST auth.govbr-email`: coleta e-mail alternativo quando o e-mail do Gov.br já está em uso (criação de conta).

## Testes
Regras de conta Gov.br (CPF / e-mail único) têm testes unitários em `tests/`:

### Suíte integrada

Testes que exercitam o `Provider` precisam do core carregado, e por isso rodam na stack de testes do repositório principal com um override que define este plugin como provedor de autenticação. Os comandos abaixo são executados a partir de `tests/` do repositório principal.

```bash
cd tests

# build (necessário na primeira vez ou após mudanças no Dockerfile/composer)
docker compose build

# suíte inteira do plugin
docker compose -f docker-compose.yml -f ../src/plugins/MultipleLocalAuth/tests/docker-compose.yml \
run --rm mapas pu /var/www/tests/MultipleLocalAuth

# um arquivo
docker compose -f docker-compose.yml -f ../src/plugins/MultipleLocalAuth/tests/docker-compose.yml \
run --rm mapas pu /var/www/tests/MultipleLocalAuth/SmokeTest.php

# um método
docker compose -f docker-compose.yml -f ../src/plugins/MultipleLocalAuth/tests/docker-compose.yml \
run --rm mapas pu /var/www/tests/MultipleLocalAuth/SmokeTest.php --filter "nomeDoMetodo"
```

O override troca apenas `auth.provider` e `auth.config`; o plugin já consta na lista base de plugins. Aponte o `pu` sempre para `/var/www/tests/MultipleLocalAuth` — apontar para `/var/www/tests` rodaria também os testes do core, que assumem o provedor de teste padrão.

### Testes unitários isolados

Regras de conta Gov.br (CPF / e-mail único) não dependem do core e têm testes próprios em `tests/`:

```bash
cd plugins/MultipleLocalAuth
php composer.phar install # ou: composer install
./vendor/bin/phpunit
./vendor/bin/phpunit tests/GovBrAccountServiceTest.php
```

## Componentes que acompanham o plugin
Expand Down
22 changes: 22 additions & 0 deletions tests/config.d/auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

// Restaura este plugin como provedor de autenticação da suíte, sobrescrevendo o
// 'auth.provider' => 'Test' que o core define em tests/config.d/auth.php. Sem isso,
// $app->auth não é a instância de MultipleLocalAuth\Provider e nada do plugin é exercitado.
return [
'auth.provider' => '\MultipleLocalAuth\Provider',

// O construtor do Provider usa `$config += [...]`, então as chaves definidas aqui
// preservam seus valores e as demais recebem os defaults.
'auth.config' => [
'salt' => 'multiplelocalauth-tests',

// Sem estratégias, usingSocialLogin() retorna false e o Opauth não é instanciado.
// O construtor do Opauth lê $_SERVER['HTTP_HOST'] e $_SERVER['REQUEST_URI'] sem
// guard, que não existem sob CLI e poluiriam o bootstrap do PHPUnit com warnings.
'strategies' => [],

// Exige validação de conta por e-mail, que é o comportamento sob teste.
'userMustConfirmEmailToUseTheSystem' => true,
],
];
22 changes: 22 additions & 0 deletions tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
services:
mapas:
volumes:
# Restaura o provedor de autenticação deste plugin na suíte de testes do repositório
# principal, sem editar tests/config.d/ do core. O core força 'auth.provider' => 'Test'
# em tests/config.d/auth.php; "zz-multiplelocalauth.d" ordena depois de "config.d" no
# glob de src/conf/config.php, então as chaves deste diretório vencem.
# Diferente de AldirBlanc e MapasBlame, aqui NÃO é preciso um config.d/plugins.php:
# config/plugins.php já lista MultipleLocalAuth e nenhum config.d de teste sobrescreve
# a chave 'plugins'.
# Caminho relativo ao cwd esperado do `docker compose` (tests/ do repositório principal).
# Monta o DIRETÓRIO (não o arquivo isolado) — bind mount de arquivo único dentro de outro
# bind mount (../config:/var/www/config) falha no Docker Desktop/virtiofs (macOS) quando o
# subdiretório de destino ainda não existe no host.
- ../src/plugins/MultipleLocalAuth/tests/config.d:/var/www/config/zz-multiplelocalauth.d

# Testes do próprio plugin, montados como subpasta de /var/www/tests (mesmo destino de
# tests/src do core, mapeado por tests/docker-compose.yml). O autoload PSR-4 'Tests\\' =>
# '/var/www/tests' (composer.json) resolve "Tests\MultipleLocalAuth\SmokeTest" para
# /var/www/tests/MultipleLocalAuth/SmokeTest.php, então as classes deste plugin podem
# usar/estender Tests\Abstract\TestCase e Tests\Traits\* normalmente.
- ../src/plugins/MultipleLocalAuth/tests/src:/var/www/tests/MultipleLocalAuth
124 changes: 124 additions & 0 deletions tests/src/AccountRegistrationEmailTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

namespace Tests\MultipleLocalAuth;

use MapasCulturais\App;
use MapasCulturais\Request;
use MultipleLocalAuth\Provider;
use Tests\Abstract\TestCase;
use Tests\Mailer\TestTransport;
use Tests\Traits\RequestFactory;

/**
* Cobre o cadastro, que delega a geração do token e o envio do e-mail aos métodos
* extraídos de dentro dele.
*
* Sem este teste, trocar os argumentos ou a ordem dessas chamadas passaria pela
* suíte: os demais testes exercitam os métodos diretamente, nunca pelo cadastro.
*/
class AccountRegistrationEmailTest extends TestCase
{
use RequestFactory;

private ?Request $originalRequest = null;
private array $originalPost = [];

protected function setUp(): void
{
parent::setUp();

TestTransport::reset();

$this->app->clearHooks('mailer.transport');
$this->app->hook('mailer.transport', function (&$transport) {
$transport = new TestTransport();
});

// App::reset() não restaura a requisição, que precisa existir para o doRegister.
$this->originalRequest = $this->app->request;
$this->originalPost = $_POST;
}

protected function tearDown(): void
{
App::i()->request = $this->originalRequest;
$_POST = $this->originalPost;

TestTransport::reset();

parent::tearDown();
}

private function register(string $email): array
{
$payload = [
'name' => 'Cadastro de Teste',
'email' => $email,
'cpf' => '111.444.777-35',
'password' => 'Senha@123',
'confirm_password' => 'Senha@123',
];

// Request::post() lê a superglobal $_POST, não o corpo da requisição PSR-7,
// então é ela que precisa ser preenchida para exercitar o cadastro.
$_POST = $payload;
$this->app->request = $this->requestFactory->mapasPOST('auth', 'register', [], [], $payload);

$result = $this->app->auth->doRegister();

$this->assertTrue(
$result['success'] ?? false,
'Cadastro rejeitado: ' . json_encode($result['errors'] ?? [], JSON_UNESCAPED_UNICODE)
);

return $result;
}

private function lastEmailBody(): string
{
return TestTransport::getLastMessage()->getOriginalMessage()->getHtmlBody();
}

function testCadastroEnviaUmEmailDeValidacaoParaOEnderecoInformado()
{
$email = 'cadastro-' . uniqid() . '@example.test';

$result = $this->register($email);

$this->assertTrue($result['success']);
$this->assertSame(1, TestTransport::getMessagesCount());

$message = TestTransport::getLastMessage()->getOriginalMessage();

$this->assertSame($email, $message->getTo()[0]->getAddress());
}

/**
* Amarra as duas extrações: o token que vai no link tem que ser o mesmo que
* ficou gravado no usuário, senão o link enviado não valida conta nenhuma.
*/
function testTokenDoLinkEOMesmoQueFicaGravadoNoUsuario()
{
$email = 'cadastro-' . uniqid() . '@example.test';

$this->register($email);

$user = $this->app->repo('User')->findOneBy(['email' => $email]);
$token = $user->getMetadata(Provider::$tokenVerifyAccountMetadata);

$this->assertNotEmpty($token);
$this->assertStringContainsString('confirma-email?token=' . $token, $this->lastEmailBody());
}

function testContaNasceAguardandoValidacao()
{
$email = 'cadastro-' . uniqid() . '@example.test';

$this->register($email);

$user = $this->app->repo('User')->findOneBy(['email' => $email]);

$this->assertSame('0', $user->getMetadata(Provider::$accountIsActiveMetadata));
$this->assertFalse($this->app->auth->isAccountValidated($user));
}
}
Loading