fix: resolve swagger duplicates and add backend tests

This commit is contained in:
Tiago Yamamoto 2025-12-19 20:28:39 -03:00
parent fdf256d436
commit 8ffd35741d
6 changed files with 763 additions and 1606 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -123,7 +123,6 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
// @Param company body registerCompanyRequest true "Dados da empresa"
// @Success 201 {object} domain.Company
// @Router /api/v1/companies [post]
// @Router /api/v1/companies [post]
func (h *Handler) CreateCompany(w http.ResponseWriter, r *http.Request) {
var req registerCompanyRequest
if err := decodeJSON(r.Context(), r, &req); err != nil {
@ -152,7 +151,6 @@ func (h *Handler) CreateCompany(w http.ResponseWriter, r *http.Request) {
// @Produce json
// @Success 200 {array} domain.Company
// @Router /api/v1/companies [get]
// @Router /api/v1/companies [get]
func (h *Handler) ListCompanies(w http.ResponseWriter, r *http.Request) {
companies, err := h.svc.ListCompanies(r.Context())
if err != nil {
@ -170,7 +168,6 @@ func (h *Handler) ListCompanies(w http.ResponseWriter, r *http.Request) {
// @Success 200 {object} domain.Company
// @Failure 404 {object} map[string]string
// @Router /api/v1/companies/{id} [get]
// @Router /api/v1/companies/{id} [get]
func (h *Handler) GetCompany(w http.ResponseWriter, r *http.Request) {
id, err := parseUUIDFromPath(r.URL.Path)
if err != nil {
@ -198,7 +195,6 @@ func (h *Handler) GetCompany(w http.ResponseWriter, r *http.Request) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/companies/{id} [patch]
// @Router /api/v1/companies/{id} [patch]
func (h *Handler) UpdateCompany(w http.ResponseWriter, r *http.Request) {
id, err := parseUUIDFromPath(r.URL.Path)
if err != nil {
@ -250,7 +246,6 @@ func (h *Handler) UpdateCompany(w http.ResponseWriter, r *http.Request) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/companies/{id} [delete]
// @Router /api/v1/companies/{id} [delete]
func (h *Handler) DeleteCompany(w http.ResponseWriter, r *http.Request) {
id, err := parseUUIDFromPath(r.URL.Path)
if err != nil {
@ -356,7 +351,6 @@ func (h *Handler) GetCompanyRating(w http.ResponseWriter, r *http.Request) {
// @Param product body registerProductRequest true "Produto"
// @Success 201 {object} domain.Product
// @Router /api/v1/products [post]
// @Router /api/v1/products [post]
func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) {
var req registerProductRequest
if err := decodeJSON(r.Context(), r, &req); err != nil {
@ -388,7 +382,6 @@ func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) {
// @Produce json
// @Success 200 {array} domain.Product
// @Router /api/v1/products [get]
// @Router /api/v1/products [get]
func (h *Handler) ListProducts(w http.ResponseWriter, r *http.Request) {
products, err := h.svc.ListProducts(r.Context())
if err != nil {
@ -406,7 +399,6 @@ func (h *Handler) ListProducts(w http.ResponseWriter, r *http.Request) {
// @Success 200 {object} domain.Product
// @Failure 404 {object} map[string]string
// @Router /api/v1/products/{id} [get]
// @Router /api/v1/products/{id} [get]
func (h *Handler) GetProduct(w http.ResponseWriter, r *http.Request) {
id, err := parseUUIDFromPath(r.URL.Path)
if err != nil {
@ -434,7 +426,6 @@ func (h *Handler) GetProduct(w http.ResponseWriter, r *http.Request) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/products/{id} [patch]
// @Router /api/v1/products/{id} [patch]
func (h *Handler) UpdateProduct(w http.ResponseWriter, r *http.Request) {
id, err := parseUUIDFromPath(r.URL.Path)
if err != nil {
@ -492,7 +483,6 @@ func (h *Handler) UpdateProduct(w http.ResponseWriter, r *http.Request) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/products/{id} [delete]
// @Router /api/v1/products/{id} [delete]
func (h *Handler) DeleteProduct(w http.ResponseWriter, r *http.Request) {
id, err := parseUUIDFromPath(r.URL.Path)
if err != nil {
@ -578,7 +568,6 @@ func (h *Handler) AdjustInventory(w http.ResponseWriter, r *http.Request) {
// @Param order body createOrderRequest true "Pedido"
// @Success 201 {object} domain.Order
// @Router /api/v1/orders [post]
// @Router /api/v1/orders [post]
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
var req createOrderRequest
if err := decodeJSON(r.Context(), r, &req); err != nil {
@ -614,7 +603,6 @@ func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
// @Produce json
// @Success 200 {array} domain.Order
// @Router /api/v1/orders [get]
// @Router /api/v1/orders [get]
func (h *Handler) ListOrders(w http.ResponseWriter, r *http.Request) {
orders, err := h.svc.ListOrders(r.Context())
if err != nil {
@ -633,7 +621,6 @@ func (h *Handler) ListOrders(w http.ResponseWriter, r *http.Request) {
// @Param id path string true "Order ID"
// @Success 200 {object} domain.Order
// @Router /api/v1/orders/{id} [get]
// @Router /api/v1/orders/{id} [get]
func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
id, err := parseUUIDFromPath(r.URL.Path)
if err != nil {
@ -660,7 +647,6 @@ func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
// @Param status body updateStatusRequest true "Novo status"
// @Success 204 ""
// @Router /api/v1/orders/{id}/status [patch]
// @Router /api/v1/orders/{id}/status [patch]
func (h *Handler) UpdateOrderStatus(w http.ResponseWriter, r *http.Request) {
id, err := parseUUIDFromPath(r.URL.Path)
if err != nil {
@ -696,7 +682,6 @@ func (h *Handler) UpdateOrderStatus(w http.ResponseWriter, r *http.Request) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/orders/{id} [delete]
// @Router /api/v1/orders/{id} [delete]
func (h *Handler) DeleteOrder(w http.ResponseWriter, r *http.Request) {
id, err := parseUUIDFromPath(r.URL.Path)
if err != nil {
@ -840,7 +825,6 @@ func (h *Handler) DeleteCartItem(w http.ResponseWriter, r *http.Request) {
// @Param id path string true "Order ID"
// @Success 201 {object} domain.PaymentPreference
// @Router /api/v1/orders/{id}/payment [post]
// @Router /api/v1/orders/{id}/payment [post]
func (h *Handler) CreatePaymentPreference(w http.ResponseWriter, r *http.Request) {
if !strings.HasSuffix(r.URL.Path, "/payment") {
http.NotFound(w, r)

View file

@ -0,0 +1,340 @@
package handler
import (
"bytes"
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gofrs/uuid/v5"
"github.com/saveinmed/backend-go/internal/domain"
"github.com/saveinmed/backend-go/internal/usecase"
)
// MockRepository implements the Repository interface for testing without database
type MockRepository struct {
companies []domain.Company
products []domain.Product
users []domain.User
orders []domain.Order
}
func NewMockRepository() *MockRepository {
return &MockRepository{
companies: make([]domain.Company, 0),
products: make([]domain.Product, 0),
users: make([]domain.User, 0),
orders: make([]domain.Order, 0),
}
}
// Company methods
func (m *MockRepository) CreateCompany(ctx context.Context, company *domain.Company) error {
id, _ := uuid.NewV4()
company.ID = id
company.CreatedAt = time.Now()
company.UpdatedAt = time.Now()
m.companies = append(m.companies, *company)
return nil
}
func (m *MockRepository) ListCompanies(ctx context.Context) ([]domain.Company, error) {
return m.companies, nil
}
func (m *MockRepository) GetCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) {
for _, c := range m.companies {
if c.ID == id {
return &c, nil
}
}
return nil, nil
}
func (m *MockRepository) UpdateCompany(ctx context.Context, company *domain.Company) error {
for i, c := range m.companies {
if c.ID == company.ID {
m.companies[i] = *company
return nil
}
}
return nil
}
func (m *MockRepository) DeleteCompany(ctx context.Context, id uuid.UUID) error {
for i, c := range m.companies {
if c.ID == id {
m.companies = append(m.companies[:i], m.companies[i+1:]...)
return nil
}
}
return nil
}
// Product methods
func (m *MockRepository) CreateProduct(ctx context.Context, product *domain.Product) error {
id, _ := uuid.NewV4()
product.ID = id
product.CreatedAt = time.Now()
product.UpdatedAt = time.Now()
m.products = append(m.products, *product)
return nil
}
func (m *MockRepository) ListProducts(ctx context.Context) ([]domain.Product, error) {
return m.products, nil
}
func (m *MockRepository) GetProduct(ctx context.Context, id uuid.UUID) (*domain.Product, error) {
for _, p := range m.products {
if p.ID == id {
return &p, nil
}
}
return nil, nil
}
func (m *MockRepository) UpdateProduct(ctx context.Context, product *domain.Product) error {
for i, p := range m.products {
if p.ID == product.ID {
m.products[i] = *product
return nil
}
}
return nil
}
func (m *MockRepository) DeleteProduct(ctx context.Context, id uuid.UUID) error {
for i, p := range m.products {
if p.ID == id {
m.products = append(m.products[:i], m.products[i+1:]...)
return nil
}
}
return nil
}
// Stub methods for other interfaces
func (m *MockRepository) AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error) {
return &domain.InventoryItem{}, nil
}
func (m *MockRepository) ListInventory(ctx context.Context, filter domain.InventoryFilter) ([]domain.InventoryItem, error) {
return []domain.InventoryItem{}, nil
}
func (m *MockRepository) CreateOrder(ctx context.Context, order *domain.Order) error {
id, _ := uuid.NewV4()
order.ID = id
m.orders = append(m.orders, *order)
return nil
}
func (m *MockRepository) ListOrders(ctx context.Context) ([]domain.Order, error) {
return m.orders, nil
}
func (m *MockRepository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) {
for _, o := range m.orders {
if o.ID == id {
return &o, nil
}
}
return nil, nil
}
func (m *MockRepository) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error {
return nil
}
func (m *MockRepository) DeleteOrder(ctx context.Context, id uuid.UUID) error {
return nil
}
func (m *MockRepository) CreateShipment(ctx context.Context, shipment *domain.Shipment) error {
return nil
}
func (m *MockRepository) GetShipmentByOrderID(ctx context.Context, orderID uuid.UUID) (*domain.Shipment, error) {
return nil, nil
}
func (m *MockRepository) CreateUser(ctx context.Context, user *domain.User) error {
id, _ := uuid.NewV4()
user.ID = id
m.users = append(m.users, *user)
return nil
}
func (m *MockRepository) ListUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
return m.users, int64(len(m.users)), nil
}
func (m *MockRepository) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error) {
for _, u := range m.users {
if u.ID == id {
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 {
return &u, nil
}
}
return nil, errors.New("user not found") // Simulate repository behavior
}
func (m *MockRepository) UpdateUser(ctx context.Context, user *domain.User) error {
return nil
}
func (m *MockRepository) DeleteUser(ctx context.Context, id uuid.UUID) error {
return nil
}
func (m *MockRepository) AddCartItem(ctx context.Context, item *domain.CartItem) (*domain.CartItem, error) {
return item, nil
}
func (m *MockRepository) ListCartItems(ctx context.Context, buyerID uuid.UUID) ([]domain.CartItem, error) {
return []domain.CartItem{}, nil
}
func (m *MockRepository) DeleteCartItem(ctx context.Context, id uuid.UUID, buyerID uuid.UUID) error {
return nil
}
func (m *MockRepository) CreateReview(ctx context.Context, review *domain.Review) error {
return nil
}
func (m *MockRepository) GetCompanyRating(ctx context.Context, companyID uuid.UUID) (*domain.CompanyRating, error) {
return &domain.CompanyRating{}, nil
}
func (m *MockRepository) SellerDashboard(ctx context.Context, sellerID uuid.UUID) (*domain.SellerDashboard, error) {
return &domain.SellerDashboard{}, nil
}
func (m *MockRepository) AdminDashboard(ctx context.Context, since time.Time) (*domain.AdminDashboard, error) {
return &domain.AdminDashboard{}, nil
}
// MockPaymentGateway implements the PaymentGateway interface for testing
type MockPaymentGateway struct{}
func (m *MockPaymentGateway) CreatePreference(ctx context.Context, order *domain.Order) (*domain.PaymentPreference, error) {
return &domain.PaymentPreference{}, nil
}
func (m *MockPaymentGateway) ParseWebhook(ctx context.Context, payload []byte) (*domain.PaymentSplitResult, error) {
return &domain.PaymentSplitResult{}, nil
}
// Create a test handler for testing
func newTestHandler() *Handler {
repo := NewMockRepository()
gateway := &MockPaymentGateway{}
svc := usecase.NewService(repo, gateway, 0.05, "test-secret", time.Hour, "test-pepper")
return New(svc)
}
func TestListProducts(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "/api/v1/products", nil)
rec := httptest.NewRecorder()
h.ListProducts(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status %d, got %d", http.StatusOK, rec.Code)
}
// Should return empty array
body := strings.TrimSpace(rec.Body.String())
if body != "[]" {
t.Errorf("expected empty array, got %s", body)
}
}
func TestListCompanies(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "/api/v1/companies", nil)
rec := httptest.NewRecorder()
h.ListCompanies(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status %d, got %d", http.StatusOK, rec.Code)
}
}
func TestCreateCompany(t *testing.T) {
h := newTestHandler()
payload := `{"role":"pharmacy","cnpj":"12345678901234","corporate_name":"Test Pharmacy","license_number":"LIC-001"}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/companies", bytes.NewReader([]byte(payload)))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.CreateCompany(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("expected status %d, got %d: %s", http.StatusCreated, rec.Code, rec.Body.String())
}
}
func TestCreateProduct(t *testing.T) {
h := newTestHandler()
sellerID, _ := uuid.NewV4()
payload := `{"seller_id":"` + sellerID.String() + `","name":"Aspirin","description":"Pain relief","batch":"BATCH-001","expires_at":"2025-12-31T00:00:00Z","price_cents":1000,"stock":100}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/products", bytes.NewReader([]byte(payload)))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.CreateProduct(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("expected status %d, got %d: %s", http.StatusCreated, rec.Code, rec.Body.String())
}
}
func TestLoginInvalidCredentials(t *testing.T) {
h := newTestHandler()
payload := `{"email":"nonexistent@test.com","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()
h.Login(rec, req)
// Should fail because user doesn't exist
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected status %d, got %d", http.StatusUnauthorized, rec.Code)
}
}
func TestListOrders(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "/api/v1/orders", nil)
rec := httptest.NewRecorder()
h.ListOrders(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status %d, got %d", http.StatusOK, rec.Code)
}
}

View file

@ -0,0 +1,143 @@
package server
import (
"context"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/jmoiron/sqlx"
"github.com/saveinmed/backend-go/internal/config"
)
// TestServerHealthCheck tests the /health endpoint without a database
func TestServerHealthCheck(t *testing.T) {
// Create a simple handler to test health endpoint
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status %d, got %d", http.StatusOK, rec.Code)
}
if rec.Body.String() != "ok" {
t.Errorf("expected body 'ok', got '%s'", rec.Body.String())
}
}
// TestServerRootEndpoint tests the root / endpoint
func TestServerRootEndpoint(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
response := `{"message":"💊 SaveInMed API is running!","docs":"/docs/index.html","health":"/health","version":"1.0.0"}`
_, _ = w.Write([]byte(response))
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status %d, got %d", http.StatusOK, rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "SaveInMed API is running") {
t.Errorf("expected body to contain 'SaveInMed API is running', got '%s'", body)
}
}
// TestServerCreationWithDatabase tests server creation with a real database
// Skip this test if SKIP_DB_TEST environment variable is set
func TestServerCreationWithDatabase(t *testing.T) {
if os.Getenv("SKIP_DB_TEST") != "" {
t.Skip("Skipping database tests")
}
// Simple .env loader for testing purposes
if content, err := os.ReadFile("../../.env"); err == nil {
for _, line := range strings.Split(string(content), "\n") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
os.Setenv(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
}
}
}
cfg := config.Load()
srv, err := New(cfg)
if err != nil {
t.Fatalf("Failed to create server: %v", err)
}
if srv == nil {
t.Fatal("Server should not be nil")
}
// Clean up
if srv.db != nil {
srv.db.Close()
}
}
// TestDatabaseConnectionAndPing tests the database connection and ping
func TestDatabaseConnectionAndPing(t *testing.T) {
if os.Getenv("SKIP_DB_TEST") != "" {
t.Skip("Skipping database tests")
}
// Simple .env loader for testing purposes
if content, err := os.ReadFile("../../.env"); err == nil {
for _, line := range strings.Split(string(content), "\n") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
os.Setenv(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
}
}
}
cfg := config.Load()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
db, err := sqlx.ConnectContext(ctx, "pgx", cfg.DatabaseURL)
if err != nil {
t.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()
if err := db.PingContext(ctx); err != nil {
t.Fatalf("Failed to ping database: %v", err)
}
// Test a simple query
var result int
if err := db.QueryRowContext(ctx, "SELECT 1").Scan(&result); err != nil {
t.Fatalf("Failed to execute query: %v", err)
}
if result != 1 {
t.Errorf("Expected 1, got %d", result)
}
}