saveinmed/backend/internal/usecase/auth_usecase_test.go

290 lines
8.5 KiB
Go

package usecase
// auth_usecase_test.go — Testes de unidade: fluxos de autenticação
//
// Cobre:
// - Login com credenciais corretas
// - Login com senha errada
// - Login com usuário inexistente
// - Login com campos vazios
// - Geração de token JWT
// - Refresh de token válido e inválido
// - Reset de senha
//
// Execute com:
// go test ./internal/usecase/... -run TestAuth -v
import (
"context"
"strings"
"testing"
"time"
"golang.org/x/crypto/bcrypt"
"github.com/gofrs/uuid/v5"
"github.com/saveinmed/backend-go/internal/domain"
"github.com/saveinmed/backend-go/internal/infrastructure/notifications"
)
const (
testJWTSecret = "test-secret"
testPepper = "test-pepper"
testTokenTTL = time.Hour
)
// newAuthTestService cria um Service configurado para testes de autenticação.
func newAuthTestService() (*Service, *MockRepository) {
repo := NewMockRepository()
notify := notifications.NewLoggerNotificationService()
svc := NewService(repo, nil, nil, notify, 0.05, 0.12, testJWTSecret, testTokenTTL, testPepper)
return svc, repo
}
// makeTestUser cria e persiste um usuário com senha hasheada correta.
// O hash inclui o pepper de teste para que o Service.Login funcione.
func makeTestUser(repo *MockRepository, username, plainPassword string) domain.User {
peppered := plainPassword + testPepper
hash, err := bcrypt.GenerateFromPassword([]byte(peppered), bcrypt.MinCost)
if err != nil {
panic("erro ao hashear senha de teste: " + err.Error())
}
companyID := uuid.Must(uuid.NewV7())
u := domain.User{
ID: uuid.Must(uuid.NewV7()),
CompanyID: companyID,
Username: username,
Email: username + "@example.com",
PasswordHash: string(hash),
Role: domain.RoleAdmin,
EmailVerified: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
repo.users = append(repo.users, u)
return u
}
// =============================================================================
// Happy path — Login
// =============================================================================
func TestAuthLogin_HappyPath(t *testing.T) {
svc, repo := newAuthTestService()
makeTestUser(repo, "alice", "S3nh@Correta!")
token, expiresAt, err := svc.Login(context.Background(), "alice", "S3nh@Correta!")
if err != nil {
t.Fatalf("Login com credenciais corretas falhou: %v", err)
}
if token == "" {
t.Error("Token JWT não deve ser vazio")
}
if expiresAt.IsZero() || expiresAt.Before(time.Now()) {
t.Error("ExpiresAt deve ser no futuro")
}
// JWT deve ter 3 partes
parts := strings.Split(token, ".")
if len(parts) != 3 {
t.Errorf("JWT deve ter 3 partes (header.payload.sig), got %d: %s", len(parts), token)
}
}
func TestAuthLogin_ByEmail(t *testing.T) {
svc, repo := newAuthTestService()
u := makeTestUser(repo, "bob", "OutraSenha#99")
// Login pelo email em vez do username
token, _, err := svc.Login(context.Background(), u.Email, "OutraSenha#99")
if err != nil {
t.Fatalf("Login por email falhou: %v", err)
}
if token == "" {
t.Error("Token deve ser retornado mesmo em login por email")
}
}
// =============================================================================
// Casos de erro — Login
// =============================================================================
func TestAuthLogin_WrongPassword(t *testing.T) {
svc, repo := newAuthTestService()
makeTestUser(repo, "carol", "SenhaCorreta1")
_, _, err := svc.Login(context.Background(), "carol", "SenhaErrada2")
if err == nil {
t.Fatal("Login com senha errada deve retornar erro")
}
}
func TestAuthLogin_UserNotFound(t *testing.T) {
svc, _ := newAuthTestService()
_, _, err := svc.Login(context.Background(), "naoexiste", "qualquer")
if err == nil {
t.Fatal("Login com usuário inexistente deve retornar erro")
}
}
func TestAuthLogin_EmptyUsername(t *testing.T) {
svc, _ := newAuthTestService()
_, _, err := svc.Login(context.Background(), "", "senha")
if err == nil {
t.Fatal("Login com username vazio deve retornar erro")
}
}
func TestAuthLogin_EmptyPassword(t *testing.T) {
svc, repo := newAuthTestService()
makeTestUser(repo, "dave", "senha123")
_, _, err := svc.Login(context.Background(), "dave", "")
if err == nil {
t.Fatal("Login com senha vazia deve retornar erro")
}
}
// =============================================================================
// Refresh token
// =============================================================================
func TestAuthRefreshToken_ValidToken(t *testing.T) {
svc, repo := newAuthTestService()
makeTestUser(repo, "eve", "Pass@123")
// Obter token via login
token, _, err := svc.Login(context.Background(), "eve", "Pass@123")
if err != nil {
t.Fatalf("Setup: login falhou: %v", err)
}
// Fazer refresh com token válido
newToken, newExp, err := svc.RefreshToken(context.Background(), token)
if err != nil {
t.Fatalf("RefreshToken com token válido falhou: %v", err)
}
if newToken == "" {
t.Error("Novo token não deve ser vazio")
}
if newExp.IsZero() {
t.Error("Nova expiração não deve ser zero")
}
}
func TestAuthRefreshToken_InvalidToken(t *testing.T) {
svc, _ := newAuthTestService()
_, _, err := svc.RefreshToken(context.Background(), "token.invalido.aqui")
if err == nil {
t.Fatal("RefreshToken com token inválido deve retornar erro")
}
}
func TestAuthRefreshToken_EmptyToken(t *testing.T) {
svc, _ := newAuthTestService()
_, _, err := svc.RefreshToken(context.Background(), "")
if err == nil {
t.Fatal("RefreshToken com token vazio deve retornar erro")
}
}
// =============================================================================
// Registro de conta
// =============================================================================
func TestAuthRegisterAccount_HappyPath(t *testing.T) {
svc, _ := newAuthTestService()
company := &domain.Company{
CNPJ: "12.345.678/0001-90",
CorporateName: "Empresa Teste LTDA",
Category: "distribuidora",
LicenseNumber: "LIC-001",
}
user := &domain.User{
Username: "newuser",
Email: "newuser@test.com",
Role: domain.RoleOwner,
Name: "Novo Usuário",
}
err := svc.RegisterAccount(context.Background(), company, user, "SenhaSegura#1")
if err != nil {
t.Fatalf("RegisterAccount falhou: %v", err)
}
if user.ID == uuid.Nil {
t.Error("ID do usuário deve ter sido gerado")
}
if user.PasswordHash == "" {
t.Error("PasswordHash deve ter sido gerado")
}
if user.PasswordHash == "SenhaSegura#1" {
t.Error("PasswordHash não deve ser a senha em texto puro")
}
}
// TestAuthRegisterAccount_DuplicateUsername_MockLimitation documenta que a validação
// de username duplicado é responsabilidade da camada de banco de dados (constraint UNIQUE).
// O MockRepository em memória não implementa essa validação — em produção com PostgreSQL
// o repo.CreateUser retornaria um erro de constraint violation.
func TestAuthRegisterAccount_DuplicateUsername_MockLimitation(t *testing.T) {
svc, repo := newAuthTestService()
makeTestUser(repo, "existinguser", "pass123")
company := &domain.Company{
CNPJ: "98.765.432/0001-10",
CorporateName: "Outra Empresa LTDA",
Category: "distribuidora",
}
user := &domain.User{
Username: "existinguser", // já existe, mas o mock não valida
Email: "outro@email.com",
Role: domain.RoleOwner,
}
err := svc.RegisterAccount(context.Background(), company, user, "SenhaNew#1")
// O mock em memória não rejeita usernames duplicados — a constraint UNIQUE é do PostgreSQL.
// Este teste documenta essa limitação. Em testes de integração com DB real, deve retornar erro.
if err != nil {
t.Logf("RegisterAccount retornou erro para username duplicado: %v", err)
} else {
t.Logf("NOTA: MockRepository não valida username duplicado — constraint UNIQUE é do PostgreSQL. "+
"Em produção, este caso retornaria erro.")
}
// Sem assert rígido — comportamento depende da implementação do repo (mock vs real)
}
// =============================================================================
// Reset de senha
// =============================================================================
func TestAuthPasswordReset_InvalidToken(t *testing.T) {
svc, _ := newAuthTestService()
err := svc.ResetPassword(context.Background(), "token-invalido", "NovaSenha#1")
if err == nil {
t.Fatal("ResetPassword com token inválido deve retornar erro")
}
}
func TestAuthPasswordReset_EmptyToken(t *testing.T) {
svc, _ := newAuthTestService()
err := svc.ResetPassword(context.Background(), "", "NovaSenha#1")
if err == nil {
t.Fatal("ResetPassword com token vazio deve retornar erro")
}
}