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") } }