fix(backend): fix build errors, update tests, and improve documentation
- Add GetUserByEmail to Repository interface for password reset flow - Add username to UpdateUser query - Fix config_test.go: remove references to deleted DB pool fields - Fix handler_test.go: add GetUserByUsername to MockRepository - Fix usecase_test.go: add GetUserByUsername and update auth tests - Update backend README with auth and admin seeding info - Create seeder-api README with usage and warnings
This commit is contained in:
parent
4612172b3c
commit
9997aed18a
7 changed files with 215 additions and 43 deletions
|
|
@ -83,8 +83,41 @@ backend/
|
|||
### Variáveis de Ambiente
|
||||
|
||||
```bash
|
||||
# Banco de Dados
|
||||
DATABASE_URL=postgres://user:password@localhost:5432/saveinmed?sslmode=disable
|
||||
PORT=8080
|
||||
|
||||
# Servidor
|
||||
BACKEND_PORT=8214
|
||||
|
||||
# Autenticação
|
||||
JWT_SECRET=your-secret-key
|
||||
JWT_EXPIRES_IN=24h
|
||||
PASSWORD_PEPPER=your-pepper
|
||||
|
||||
# Admin Seeding (criado automaticamente na inicialização)
|
||||
ADMIN_NAME=Administrator
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@saveinmed.com
|
||||
ADMIN_PASSWORD=admin123
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS=*
|
||||
|
||||
# Mercado Pago
|
||||
MERCADOPAGO_BASE_URL=https://api.mercadopago.com
|
||||
MARKETPLACE_COMMISSION=2.5
|
||||
```
|
||||
|
||||
### Autenticação
|
||||
|
||||
A autenticação utiliza **username** (não email) para login:
|
||||
|
||||
```json
|
||||
POST /api/v1/auth/login
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
```
|
||||
|
||||
### Pré-requisitos
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ import (
|
|||
func TestLoadDefaults(t *testing.T) {
|
||||
// Clear any environment variables that might interfere
|
||||
envVars := []string{
|
||||
"APP_NAME", "BACKEND_PORT", "DATABASE_URL", "DB_MAX_OPEN_CONNS",
|
||||
"DB_MAX_IDLE_CONNS", "DB_CONN_MAX_IDLE", "MERCADOPAGO_BASE_URL",
|
||||
"MARKETPLACE_COMMISSION", "JWT_SECRET", "JWT_EXPIRES_IN",
|
||||
"PASSWORD_PEPPER", "CORS_ORIGINS",
|
||||
"APP_NAME", "BACKEND_PORT", "DATABASE_URL",
|
||||
"MERCADOPAGO_BASE_URL", "MARKETPLACE_COMMISSION", "JWT_SECRET", "JWT_EXPIRES_IN",
|
||||
"PASSWORD_PEPPER", "CORS_ORIGINS", "ADMIN_NAME", "ADMIN_USERNAME", "ADMIN_EMAIL", "ADMIN_PASSWORD",
|
||||
}
|
||||
origEnvs := make(map[string]string)
|
||||
for _, key := range envVars {
|
||||
|
|
@ -35,15 +34,6 @@ func TestLoadDefaults(t *testing.T) {
|
|||
if cfg.Port != "8214" {
|
||||
t.Errorf("expected Port '8214', got '%s'", cfg.Port)
|
||||
}
|
||||
if cfg.MaxOpenConns != 15 {
|
||||
t.Errorf("expected MaxOpenConns 15, got %d", cfg.MaxOpenConns)
|
||||
}
|
||||
if cfg.MaxIdleConns != 5 {
|
||||
t.Errorf("expected MaxIdleConns 5, got %d", cfg.MaxIdleConns)
|
||||
}
|
||||
if cfg.ConnMaxIdle != 5*time.Minute {
|
||||
t.Errorf("expected ConnMaxIdle 5m, got %v", cfg.ConnMaxIdle)
|
||||
}
|
||||
if cfg.JWTSecret != "dev-secret" {
|
||||
t.Errorf("expected JWTSecret 'dev-secret', got '%s'", cfg.JWTSecret)
|
||||
}
|
||||
|
|
@ -56,33 +46,47 @@ func TestLoadDefaults(t *testing.T) {
|
|||
if len(cfg.CORSOrigins) != 1 || cfg.CORSOrigins[0] != "*" {
|
||||
t.Errorf("expected CORSOrigins ['*'], got %v", cfg.CORSOrigins)
|
||||
}
|
||||
if cfg.AdminName != "Administrator" {
|
||||
t.Errorf("expected AdminName 'Administrator', got '%s'", cfg.AdminName)
|
||||
}
|
||||
if cfg.AdminUsername != "admin" {
|
||||
t.Errorf("expected AdminUsername 'admin', got '%s'", cfg.AdminUsername)
|
||||
}
|
||||
if cfg.AdminEmail != "admin@saveinmed.com" {
|
||||
t.Errorf("expected AdminEmail 'admin@saveinmed.com', got '%s'", cfg.AdminEmail)
|
||||
}
|
||||
if cfg.AdminPassword != "admin123" {
|
||||
t.Errorf("expected AdminPassword 'admin123', got '%s'", cfg.AdminPassword)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnv(t *testing.T) {
|
||||
os.Setenv("APP_NAME", "test-app")
|
||||
os.Setenv("BACKEND_PORT", "9999")
|
||||
os.Setenv("DATABASE_URL", "postgres://test:test@localhost:5432/test")
|
||||
os.Setenv("DB_MAX_OPEN_CONNS", "100")
|
||||
os.Setenv("DB_MAX_IDLE_CONNS", "50")
|
||||
os.Setenv("DB_CONN_MAX_IDLE", "10m")
|
||||
os.Setenv("MARKETPLACE_COMMISSION", "5.0")
|
||||
os.Setenv("JWT_SECRET", "super-secret")
|
||||
os.Setenv("JWT_EXPIRES_IN", "12h")
|
||||
os.Setenv("PASSWORD_PEPPER", "pepper123")
|
||||
os.Setenv("CORS_ORIGINS", "https://example.com,https://app.example.com")
|
||||
os.Setenv("ADMIN_NAME", "CustomAdmin")
|
||||
os.Setenv("ADMIN_USERNAME", "customadmin")
|
||||
os.Setenv("ADMIN_EMAIL", "custom@example.com")
|
||||
os.Setenv("ADMIN_PASSWORD", "securepass")
|
||||
|
||||
defer func() {
|
||||
os.Unsetenv("APP_NAME")
|
||||
os.Unsetenv("BACKEND_PORT")
|
||||
os.Unsetenv("DATABASE_URL")
|
||||
os.Unsetenv("DB_MAX_OPEN_CONNS")
|
||||
os.Unsetenv("DB_MAX_IDLE_CONNS")
|
||||
os.Unsetenv("DB_CONN_MAX_IDLE")
|
||||
os.Unsetenv("MARKETPLACE_COMMISSION")
|
||||
os.Unsetenv("JWT_SECRET")
|
||||
os.Unsetenv("JWT_EXPIRES_IN")
|
||||
os.Unsetenv("PASSWORD_PEPPER")
|
||||
os.Unsetenv("CORS_ORIGINS")
|
||||
os.Unsetenv("ADMIN_NAME")
|
||||
os.Unsetenv("ADMIN_USERNAME")
|
||||
os.Unsetenv("ADMIN_EMAIL")
|
||||
os.Unsetenv("ADMIN_PASSWORD")
|
||||
}()
|
||||
|
||||
cfg := Load()
|
||||
|
|
@ -96,15 +100,6 @@ func TestLoadFromEnv(t *testing.T) {
|
|||
if cfg.DatabaseURL != "postgres://test:test@localhost:5432/test" {
|
||||
t.Errorf("expected custom DatabaseURL, got '%s'", cfg.DatabaseURL)
|
||||
}
|
||||
if cfg.MaxOpenConns != 100 {
|
||||
t.Errorf("expected MaxOpenConns 100, got %d", cfg.MaxOpenConns)
|
||||
}
|
||||
if cfg.MaxIdleConns != 50 {
|
||||
t.Errorf("expected MaxIdleConns 50, got %d", cfg.MaxIdleConns)
|
||||
}
|
||||
if cfg.ConnMaxIdle != 10*time.Minute {
|
||||
t.Errorf("expected ConnMaxIdle 10m, got %v", cfg.ConnMaxIdle)
|
||||
}
|
||||
if cfg.MarketplaceCommission != 5.0 {
|
||||
t.Errorf("expected MarketplaceCommission 5.0, got %f", cfg.MarketplaceCommission)
|
||||
}
|
||||
|
|
@ -120,6 +115,18 @@ func TestLoadFromEnv(t *testing.T) {
|
|||
if len(cfg.CORSOrigins) != 2 {
|
||||
t.Errorf("expected 2 CORS origins, got %d", len(cfg.CORSOrigins))
|
||||
}
|
||||
if cfg.AdminName != "CustomAdmin" {
|
||||
t.Errorf("expected AdminName 'CustomAdmin', got '%s'", cfg.AdminName)
|
||||
}
|
||||
if cfg.AdminUsername != "customadmin" {
|
||||
t.Errorf("expected AdminUsername 'customadmin', got '%s'", cfg.AdminUsername)
|
||||
}
|
||||
if cfg.AdminEmail != "custom@example.com" {
|
||||
t.Errorf("expected AdminEmail 'custom@example.com', got '%s'", cfg.AdminEmail)
|
||||
}
|
||||
if cfg.AdminPassword != "securepass" {
|
||||
t.Errorf("expected AdminPassword 'securepass', got '%s'", cfg.AdminPassword)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddr(t *testing.T) {
|
||||
|
|
@ -131,28 +138,18 @@ func TestAddr(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInvalidEnvValues(t *testing.T) {
|
||||
os.Setenv("DB_MAX_OPEN_CONNS", "not-a-number")
|
||||
os.Setenv("MARKETPLACE_COMMISSION", "invalid")
|
||||
os.Setenv("DB_CONN_MAX_IDLE", "bad-duration")
|
||||
|
||||
defer func() {
|
||||
os.Unsetenv("DB_MAX_OPEN_CONNS")
|
||||
os.Unsetenv("MARKETPLACE_COMMISSION")
|
||||
os.Unsetenv("DB_CONN_MAX_IDLE")
|
||||
}()
|
||||
|
||||
cfg := Load()
|
||||
|
||||
// Should use defaults when values are invalid
|
||||
if cfg.MaxOpenConns != 15 {
|
||||
t.Errorf("expected fallback MaxOpenConns 15, got %d", cfg.MaxOpenConns)
|
||||
}
|
||||
if cfg.MarketplaceCommission != 2.5 {
|
||||
t.Errorf("expected fallback MarketplaceCommission 2.5, got %f", cfg.MarketplaceCommission)
|
||||
}
|
||||
if cfg.ConnMaxIdle != 5*time.Minute {
|
||||
t.Errorf("expected fallback ConnMaxIdle 5m, got %v", cfg.ConnMaxIdle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyCORSOrigins(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -193,6 +193,15 @@ func (m *MockRepository) GetUser(ctx context.Context, id uuid.UUID) (*domain.Use
|
|||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
func (m *MockRepository) GetUserByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||
for _, u := range m.users {
|
||||
if u.Username == username {
|
||||
return &u, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
func (m *MockRepository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
for _, u := range m.users {
|
||||
if u.Email == email {
|
||||
|
|
@ -350,7 +359,7 @@ func TestCreateProduct(t *testing.T) {
|
|||
func TestLoginInvalidCredentials(t *testing.T) {
|
||||
h := newTestHandler()
|
||||
|
||||
payload := `{"email":"nonexistent@test.com","password":"wrongpassword"}`
|
||||
payload := `{"username":"nonexistent","password":"wrongpassword"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader([]byte(payload)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
|
|
|||
|
|
@ -842,11 +842,20 @@ func (r *Repository) GetUserByUsername(ctx context.Context, username string) (*d
|
|||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
var user domain.User
|
||||
query := `SELECT id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at FROM users WHERE email = $1`
|
||||
if err := r.db.GetContext(ctx, &user, query, email); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateUser(ctx context.Context, user *domain.User) error {
|
||||
user.UpdatedAt = time.Now().UTC()
|
||||
|
||||
query := `UPDATE users
|
||||
SET company_id = :company_id, role = :role, name = :name, email = :email, email_verified = :email_verified, password_hash = :password_hash, updated_at = :updated_at
|
||||
SET company_id = :company_id, role = :role, name = :name, username = :username, email = :email, email_verified = :email_verified, password_hash = :password_hash, updated_at = :updated_at
|
||||
WHERE id = :id`
|
||||
|
||||
res, err := r.db.NamedExecContext(ctx, query, user)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ type Repository interface {
|
|||
ListUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error)
|
||||
GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error)
|
||||
GetUserByUsername(ctx context.Context, username string) (*domain.User, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||
UpdateUser(ctx context.Context, user *domain.User) error
|
||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
|
|
|
|||
|
|
@ -196,6 +196,15 @@ func (m *MockRepository) GetUser(ctx context.Context, id uuid.UUID) (*domain.Use
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockRepository) GetUserByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||
for _, u := range m.users {
|
||||
if u.Username == username {
|
||||
return &u, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockRepository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
for _, u := range m.users {
|
||||
if u.Email == email {
|
||||
|
|
@ -694,6 +703,7 @@ func TestAuthenticate(t *testing.T) {
|
|||
CompanyID: uuid.Must(uuid.NewV4()),
|
||||
Role: "admin",
|
||||
Name: "Test User",
|
||||
Username: "authuser",
|
||||
Email: "auth@example.com",
|
||||
}
|
||||
err := svc.CreateUser(ctx, user, "testpass123")
|
||||
|
|
@ -705,7 +715,7 @@ func TestAuthenticate(t *testing.T) {
|
|||
repo.users[0] = *user
|
||||
|
||||
// Test authentication
|
||||
token, expiresAt, err := svc.Authenticate(ctx, "auth@example.com", "testpass123")
|
||||
token, expiresAt, err := svc.Authenticate(ctx, "authuser", "testpass123")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to authenticate: %v", err)
|
||||
}
|
||||
|
|
@ -726,12 +736,13 @@ func TestAuthenticateInvalidPassword(t *testing.T) {
|
|||
CompanyID: uuid.Must(uuid.NewV4()),
|
||||
Role: "admin",
|
||||
Name: "Test User",
|
||||
Username: "failuser",
|
||||
Email: "fail@example.com",
|
||||
}
|
||||
svc.CreateUser(ctx, user, "correctpass")
|
||||
repo.users[0] = *user
|
||||
|
||||
_, _, err := svc.Authenticate(ctx, "fail@example.com", "wrongpass")
|
||||
_, _, err := svc.Authenticate(ctx, "failuser", "wrongpass")
|
||||
if err == nil {
|
||||
t.Error("expected authentication to fail")
|
||||
}
|
||||
|
|
|
|||
112
seeder-api/README.md
Normal file
112
seeder-api/README.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# SaveInMed Seeder API
|
||||
|
||||
Microserviço utilitário para popular o banco de dados com dados de teste para desenvolvimento e demonstração.
|
||||
|
||||
## ⚠️ AVISO IMPORTANTE
|
||||
|
||||
**Este serviço é DESTRUTIVO!** Ele:
|
||||
1. **REMOVE** todas as tabelas existentes (`companies`, `products`, `users`, etc.)
|
||||
2. **RECRIA** apenas as tabelas `companies` e `products`
|
||||
3. **NÃO RECRIA** a tabela `users` - você precisa reiniciar o backend após usar o seeder
|
||||
|
||||
## 🎯 Propósito
|
||||
|
||||
Gerar dados de teste para o marketplace SaveInMed, criando:
|
||||
- **400 farmácias** na região de Anápolis/GO
|
||||
- **20-500 produtos** por farmácia
|
||||
- Dados variados de medicamentos com preços e validades realistas
|
||||
|
||||
## 🏗️ Arquitetura
|
||||
|
||||
```
|
||||
seeder-api/
|
||||
├── main.go # Entry point HTTP (POST /seed)
|
||||
├── pkg/
|
||||
│ └── seeder/
|
||||
│ └── seeder.go # Lógica de geração de dados
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
```
|
||||
|
||||
## 📍 Dados Gerados
|
||||
|
||||
### Localização
|
||||
- Centro em **Anápolis, GO** (Lat: -16.3281, Lng: -48.9530)
|
||||
- Variação de ~5km para cada farmácia
|
||||
|
||||
### Farmácias
|
||||
- **Quantidade**: 400 empresas
|
||||
- **Categoria**: `farmacia`
|
||||
- **70%** verificadas (`is_verified = true`)
|
||||
- CNPJs gerados automaticamente
|
||||
|
||||
### Produtos (Medicamentos)
|
||||
- **20-500 produtos** por farmácia
|
||||
- **Categorias**: Analgésicos, Antibióticos, Anti-inflamatórios, Cardiovasculares, Dermatológicos, Vitaminas, Oftálmicos, Respiratórios, etc.
|
||||
- **Validade**: 30 dias a 2 anos
|
||||
- **Variação de preço**: -20% a +30% do preço base
|
||||
|
||||
## 🔧 Variáveis de Ambiente
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgres://user:password@host:port/dbname?sslmode=disable
|
||||
PORT=8216 # Porta padrão do seeder
|
||||
```
|
||||
|
||||
## 🚀 Uso
|
||||
|
||||
### Executar Localmente
|
||||
|
||||
```bash
|
||||
# Configurar DATABASE_URL
|
||||
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/saveinmed?sslmode=disable
|
||||
|
||||
# Executar
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### Endpoint
|
||||
|
||||
```bash
|
||||
# Iniciar seeding
|
||||
POST http://localhost:8216/seed
|
||||
|
||||
# Resposta de sucesso
|
||||
{
|
||||
"tenants": 400,
|
||||
"products": 85432,
|
||||
"location": "Anápolis, GO"
|
||||
}
|
||||
```
|
||||
|
||||
## ⚡ Fluxo de Uso Recomendado
|
||||
|
||||
1. **Parar o backend principal** (para evitar conflitos de conexão)
|
||||
2. **Executar o seeder**: `curl -X POST http://localhost:8216/seed`
|
||||
3. **Reiniciar o backend** (para aplicar migrations e recriar tabela `users`)
|
||||
4. A API estará pronta com dados de teste
|
||||
|
||||
## 🐳 Docker
|
||||
|
||||
```bash
|
||||
# Build
|
||||
docker build -t saveinmed-seeder:latest .
|
||||
|
||||
# Run
|
||||
docker run -p 8216:8216 \
|
||||
-e DATABASE_URL=postgres://user:password@host:5432/saveinmed \
|
||||
saveinmed-seeder:latest
|
||||
|
||||
# Seed
|
||||
curl -X POST http://localhost:8216/seed
|
||||
```
|
||||
|
||||
## 📝 Notas
|
||||
|
||||
- Os dados são **regeneráveis** - execute novamente para limpar e recriar
|
||||
- Ideal para ambientes de **desenvolvimento** e **staging**
|
||||
- **NÃO USE EM PRODUÇÃO** - vai apagar todos os dados reais!
|
||||
|
||||
## 📝 Licença
|
||||
|
||||
MIT
|
||||
Loading…
Reference in a new issue