290 lines
8.5 KiB
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: "Admin",
|
|
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: "Dono",
|
|
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: "Dono",
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|