Files
workbloomerp-api/bin/test-auth.php
T

354 lines
16 KiB
PHP

<?php
/**
* Suíte de testes automatizados do fluxo de autenticação (Workbloom ERP).
* Executa todos os cenários requeridos com validação rigorosa de regras.
*/
require_once __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
use Ramsey\Uuid\Uuid;
use KrothiumAPI\Database\DBManager;
use WorkbloomERP\Security\JwtService;
use WorkbloomERP\Security\AuthContext;
use WorkbloomERP\Middleware\AuthMiddleware;
use WorkbloomERP\Module\Auth\DTO\LoginDTO;
use WorkbloomERP\Module\Auth\Repository\AuthRepository;
use WorkbloomERP\Module\Auth\Service\AuthService;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
$db = DBManager::getConnection();
$repo = new AuthRepository($db);
$jwt = new JwtService();
$service = new AuthService($repo, $jwt);
$passed = 0;
$failed = 0;
function assertTest(string $title, bool $condition, ?string $detail = null): void {
global $passed, $failed;
if ($condition) {
echo " [PASS] {$title}\n";
$passed++;
} else {
echo " [FAIL] {$title}" . ($detail ? " - {$detail}" : "") . "\n";
$failed++;
}
}
echo "\n=======================================================\n";
echo " INICIANDO BATERIA DE TESTES DE AUTENTICAÇÃO\n";
echo "=======================================================\n\n";
// ---------------------------------------------------------------------------
// 1. Login com e-mail válido
// ---------------------------------------------------------------------------
echo "Cenário 1: Login com e-mail válido\n";
try {
$dto = LoginDTO::fromArray(['login' => 'admin@workbloom.com.br', 'senha' => 'Admin@123456']);
$res = $service->login($dto);
assertTest("Geração do token JWT", !empty($res['token']));
assertTest("Tipo do token é Bearer", $res['token_type'] === 'Bearer');
assertTest("Tempo de expiração retornado", $res['expires_in'] > 0);
assertTest("Dados do usuário presentes", isset($res['usuario']['email']) && $res['usuario']['email'] === 'admin@workbloom.com.br');
assertTest("UUID presente no usuário", !empty($res['usuario']['uuid']));
assertTest("ID numérico NÃO exposto no usuário", !isset($res['usuario']['id']));
assertTest("Senha hash NÃO exposta no retorno", !isset($res['usuario']['senha_hash']));
assertTest("Empresas vinculadas presentes", is_array($res['empresas']) && count($res['empresas']) >= 1);
assertTest("UUID presente na empresa", !empty($res['empresas'][0]['uuid']));
assertTest("ID numérico NÃO exposto na empresa", !isset($res['empresas'][0]['id']));
assertTest("Perfil ADMIN identificado na empresa", $res['empresas'][0]['perfil'] === 'ADMIN');
assertTest("Flag de acesso a todas as filiais presente", isset($res['empresas'][0]['acesso_todas_filiais']));
} catch (Throwable $e) {
assertTest("Execução do login por e-mail", false, $e->getMessage());
}
// ---------------------------------------------------------------------------
// 2. Login com nome de usuário válido
// ---------------------------------------------------------------------------
echo "\nCenário 2: Login com nome de usuário válido\n";
try {
$dto = LoginDTO::fromArray(['login' => 'admin', 'senha' => 'Admin@123456']);
$res = $service->login($dto);
assertTest("Login por username autenticado com sucesso", !empty($res['token']));
assertTest("ID do usuário consistente", $res['usuario']['nome_usuario'] === 'admin');
} catch (Throwable $e) {
assertTest("Execução do login por username", false, $e->getMessage());
}
// ---------------------------------------------------------------------------
// 3. Login com senha inválida
// ---------------------------------------------------------------------------
echo "\nCenário 3: Login com senha inválida\n";
try {
$dto = LoginDTO::fromArray(['login' => 'admin', 'senha' => 'SenhaIncorreta@999']);
$service->login($dto);
assertTest("Bloqueio de senha incorreta", false, "Deveria ter lançado RuntimeException");
} catch (RuntimeException $e) {
assertTest("Rejeitado com código 401", $e->getCode() === 401);
assertTest("Mensagem genérica de credenciais inválidas", $e->getMessage() === 'Credenciais inválidas.');
} catch (Throwable $e) {
assertTest("Bloqueio de senha incorreta", false, $e->getMessage());
}
// ---------------------------------------------------------------------------
// 4. Login com usuário inexistente (prevenção contra enumeração)
// ---------------------------------------------------------------------------
echo "\nCenário 4: Tentativa com usuário inexistente\n";
try {
$dto = LoginDTO::fromArray(['login' => 'usuario_fantasma_xyz@workbloom.com.br', 'senha' => 'Admin@123456']);
$service->login($dto);
assertTest("Bloqueio de usuário inexistente", false, "Deveria ter lançado RuntimeException");
} catch (RuntimeException $e) {
assertTest("Rejeitado com código 401", $e->getCode() === 401);
assertTest("Mesma mensagem genérica do erro de senha (anti-enumeração)", $e->getMessage() === 'Credenciais inválidas.');
} catch (Throwable $e) {
assertTest("Bloqueio de usuário inexistente", false, $e->getMessage());
}
// ---------------------------------------------------------------------------
// Criação temporária de usuários de teste para cenários avançados
// ---------------------------------------------------------------------------
$testUserUuidInativo = Uuid::uuid7()->toString();
$testUserUuidDeletado = Uuid::uuid7()->toString();
$testUserUuidSemEmpresa = Uuid::uuid7()->toString();
$testUserUuidMultiEmpresa = Uuid::uuid7()->toString();
$senhaHashPadrao = password_hash('Teste@123456', PASSWORD_ARGON2ID);
// 5. Usuário inativo
$db->insert('usuario', [
'uuid' => $testUserUuidInativo,
'nome_completo' => 'Usuário Inativo Teste',
'email' => 'inativo@workbloom.com.br',
'nome_usuario' => 'user_inativo',
'senha_hash' => $senhaHashPadrao,
'ativo' => 0,
]);
$idInativo = (int)$db->fetchColumn("SELECT id FROM usuario WHERE uuid = :uuid LIMIT 1", ['uuid' => $testUserUuidInativo]);
// 6. Usuário logicamente excluído (deleted_at)
$db->insert('usuario', [
'uuid' => $testUserUuidDeletado,
'nome_completo' => 'Usuário Deletado Teste',
'email' => 'deletado@workbloom.com.br',
'nome_usuario' => 'user_deletado',
'senha_hash' => $senhaHashPadrao,
'ativo' => 1,
]);
$idDeletado = (int)$db->fetchColumn("SELECT id FROM usuario WHERE uuid = :uuid LIMIT 1", ['uuid' => $testUserUuidDeletado]);
$db->execute("UPDATE usuario SET deleted_at = CURRENT_TIMESTAMP WHERE id = :id", ['id' => $idDeletado]);
// 7. Usuário sem vínculo com empresa
$db->insert('usuario', [
'uuid' => $testUserUuidSemEmpresa,
'nome_completo' => 'Usuário Sem Empresa',
'email' => 'semempresa@workbloom.com.br',
'nome_usuario' => 'user_semempresa',
'senha_hash' => $senhaHashPadrao,
'ativo' => 1,
]);
$idSemEmpresa = (int)$db->fetchColumn("SELECT id FROM usuario WHERE uuid = :uuid LIMIT 1", ['uuid' => $testUserUuidSemEmpresa]);
// 8. Usuário multiempresa com perfis distintos
$db->insert('usuario', [
'uuid' => $testUserUuidMultiEmpresa,
'nome_completo' => 'Usuário Multiempresa',
'email' => 'multiempresa@workbloom.com.br',
'nome_usuario' => 'user_multiempresa',
'senha_hash' => $senhaHashPadrao,
'ativo' => 1,
]);
$idMultiEmpresa = (int)$db->fetchColumn("SELECT id FROM usuario WHERE uuid = :uuid LIMIT 1", ['uuid' => $testUserUuidMultiEmpresa]);
// Cria 2 empresas adicionais de teste
$empresaUuid1 = Uuid::uuid7()->toString();
$empresaUuid2 = Uuid::uuid7()->toString();
$db->insert('empresa', [
'uuid' => $empresaUuid1,
'tipo_estabelecimento' => 'MATRIZ',
'status' => 'ATIVO',
'nome_empresarial' => 'EMPRESA ALFA LTDA',
'nome_fantasia' => 'Alfa Matriz',
'cnpj' => '11111111000101',
'regime_tributario' => 1,
]);
$idEmpresaAlfa = (int)$db->fetchColumn("SELECT id FROM empresa WHERE uuid = :uuid LIMIT 1", ['uuid' => $empresaUuid1]);
$db->insert('empresa', [
'uuid' => $empresaUuid2,
'tipo_estabelecimento' => 'FILIAL',
'status' => 'ATIVO',
'nome_empresarial' => 'EMPRESA BETA FILIAL LTDA',
'nome_fantasia' => 'Beta Filial',
'cnpj' => '11111111000202',
'regime_tributario' => 1,
]);
$idEmpresaBeta = (int)$db->fetchColumn("SELECT id FROM empresa WHERE uuid = :uuid LIMIT 1", ['uuid' => $empresaUuid2]);
// Vincula usuário multiempresa às 2 empresas com perfis diferentes
$db->insert('usuario_empresa', [
'usuario_id' => $idMultiEmpresa,
'empresa_id' => $idEmpresaAlfa,
'perfil' => 'ADMIN',
'acesso_todas_filiais' => 1,
]);
$db->insert('usuario_empresa', [
'usuario_id' => $idMultiEmpresa,
'empresa_id' => $idEmpresaBeta,
'perfil' => 'EMISSOR',
'acesso_todas_filiais' => 0,
]);
try {
// ---------------------------------------------------------------------------
// Cenário 5: Tentativa com usuário inativo
// ---------------------------------------------------------------------------
echo "\nCenário 5: Tentativa com usuário inativo (ativo = false)\n";
try {
$dto = LoginDTO::fromArray(['login' => 'inativo@workbloom.com.br', 'senha' => 'Teste@123456']);
$service->login($dto);
assertTest("Bloqueio de usuário inativo", false, "Deveria ter lançado RuntimeException 403");
} catch (RuntimeException $e) {
assertTest("Rejeitado com código 403", $e->getCode() === 403);
assertTest("Mensagem indica usuário inativo", str_contains($e->getMessage(), 'inativo'));
}
// ---------------------------------------------------------------------------
// Cenário 6: Tentativa com usuário com deleted_at preenchido
// ---------------------------------------------------------------------------
echo "\nCenário 6: Tentativa com usuário excluído (deleted_at preenchido)\n";
try {
$dto = LoginDTO::fromArray(['login' => 'deletado@workbloom.com.br', 'senha' => 'Teste@123456']);
$service->login($dto);
assertTest("Bloqueio de usuário excluído", false, "Deveria ter lançado RuntimeException 401");
} catch (RuntimeException $e) {
assertTest("Rejeitado como não encontrado (401)", $e->getCode() === 401);
assertTest("Mensagem genérica de credenciais inválidas", $e->getMessage() === 'Credenciais inválidas.');
}
// ---------------------------------------------------------------------------
// Cenário 7: Usuário sem vínculo com empresa
// ---------------------------------------------------------------------------
echo "\nCenário 7: Usuário sem vínculo com empresa\n";
try {
$dto = LoginDTO::fromArray(['login' => 'semempresa@workbloom.com.br', 'senha' => 'Teste@123456']);
$res = $service->login($dto);
assertTest("Login efetuado mesmo sem empresa vinculada", !empty($res['token']));
assertTest("Lista de empresas vazia", is_array($res['empresas']) && count($res['empresas']) === 0);
} catch (Throwable $e) {
assertTest("Login sem empresa", false, $e->getMessage());
}
// ---------------------------------------------------------------------------
// Cenário 8: Usuário multiempresa com múltiplos perfis
// ---------------------------------------------------------------------------
echo "\nCenário 8: Usuário com múltiplas empresas e perfis distintos\n";
try {
$dto = LoginDTO::fromArray(['login' => 'multiempresa@workbloom.com.br', 'senha' => 'Teste@123456']);
$res = $service->login($dto);
assertTest("Autenticação com sucesso", !empty($res['token']));
assertTest("Exatamente 2 empresas vinculadas", count($res['empresas']) === 2);
$perfilMap = [];
$filiaisMap = [];
foreach ($res['empresas'] as $emp) {
$perfilMap[$emp['nome_fantasia']] = $emp['perfil'];
$filiaisMap[$emp['nome_fantasia']] = (bool)$emp['acesso_todas_filiais'];
}
assertTest("Perfil ADMIN na Empresa Alfa", ($perfilMap['Alfa Matriz'] ?? null) === 'ADMIN');
assertTest("Flag todas as filiais = true na Empresa Alfa", ($filiaisMap['Alfa Matriz'] ?? null) === true);
assertTest("Perfil EMISSOR na Empresa Beta", ($perfilMap['Beta Filial'] ?? null) === 'EMISSOR');
assertTest("Flag todas as filiais = false na Empresa Beta", ($filiaisMap['Beta Filial'] ?? null) === false);
} catch (Throwable $e) {
assertTest("Multiempresa", false, $e->getMessage());
}
// ---------------------------------------------------------------------------
// Cenário 9: Validação do Token JWT e Middleware
// ---------------------------------------------------------------------------
echo "\nCenário 9: Validação do Token JWT e Middleware de Proteção\n";
// Token forjado / adulterado
try {
$jwt->validateToken('token.invalido.falso');
assertTest("Rejeição de token adulterado", false, "Deveria ter lançado RuntimeException");
} catch (RuntimeException $e) {
assertTest("Token forjado rejeitado com código 401", $e->getCode() === 401);
}
// Token expirado
try {
$expiredJwt = new JwtService(expiresIn: -10);
$expiredData = $expiredJwt->generateToken(['id' => 1, 'sub' => 'uuid-teste']);
$jwt->validateToken($expiredData['token']);
assertTest("Rejeição de token expirado", false, "Deveria ter lançado RuntimeException");
} catch (RuntimeException $e) {
assertTest("Token expirado rejeitado com código 401", $e->getCode() === 401);
}
// Middleware sem header
unset($_SERVER['HTTP_AUTHORIZATION']);
$middleware = new AuthMiddleware($jwt);
$midRes = $middleware->handle();
assertTest("Middleware bloqueia requisição sem token", is_array($midRes) && ($midRes['block'] ?? false) === true);
assertTest("Código retornado pelo middleware é 401", ($midRes['response_code'] ?? null) === 401);
// Middleware com token válido
$tokenValido = $res['token'];
$_SERVER['HTTP_AUTHORIZATION'] = "Bearer {$tokenValido}";
AuthContext::clear();
$midResValido = $middleware->handle();
assertTest("Middleware aceita requisição com Bearer Token válido", $midResValido === true);
assertTest("AuthContext populado com o UUID do usuário", AuthContext::getUserUuid() === $testUserUuidMultiEmpresa);
// ---------------------------------------------------------------------------
// Cenário 10: Consulta ao Perfil (endpoint me / getProfile)
// ---------------------------------------------------------------------------
echo "\nCenário 10: Consulta ao Perfil do usuário autenticado (GET /api/v1/auth/me)\n";
try {
$profile = $service->getProfile(AuthContext::getUserUuid());
assertTest("Perfil recuperado com sucesso via UUID", !empty($profile['usuario']['uuid']));
assertTest("ID numérico NÃO exposto no perfil do usuário", !isset($profile['usuario']['id']));
assertTest("Nome completo presente", $profile['usuario']['nome_completo'] === 'Usuário Multiempresa');
assertTest("Senha hash NÃO exposta no perfil", !isset($profile['usuario']['senha_hash']));
assertTest("Lista de empresas presente no perfil", count($profile['empresas']) === 2);
assertTest("ID numérico NÃO exposto nas empresas do perfil", !isset($profile['empresas'][0]['id']));
assertTest("UUID presente nas empresas do perfil", !empty($profile['empresas'][0]['uuid']));
} catch (Throwable $e) {
assertTest("Consulta de perfil", false, $e->getMessage());
}
} finally {
// Limpeza dos dados temporários criados nos testes
$db->execute("DELETE FROM usuario_empresa WHERE usuario_id IN (:u1, :u2, :u3, :u4)", [
'u1' => $idInativo,
'u2' => $idDeletado,
'u3' => $idSemEmpresa,
'u4' => $idMultiEmpresa,
]);
$db->execute("DELETE FROM usuario WHERE id IN (:u1, :u2, :u3, :u4)", [
'u1' => $idInativo,
'u2' => $idDeletado,
'u3' => $idSemEmpresa,
'u4' => $idMultiEmpresa,
]);
$db->execute("DELETE FROM empresa WHERE id IN (:e1, :e2)", [
'e1' => $idEmpresaAlfa,
'e2' => $idEmpresaBeta,
]);
unset($_SERVER['HTTP_AUTHORIZATION']);
AuthContext::clear();
}
echo "\n=======================================================\n";
echo " RESULTADO DOS TESTES: {$passed} passaram, {$failed} falharam\n";
echo "=======================================================\n\n";
if ($failed > 0) {
exit(1);
}