Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ node_modules/
Product Backlog.pdf

## Upload Files
uploads/trabajos-cientificos/
uploads/*
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,34 @@ make dev server # frontend local, API remoto en Koyeb (usa .env en raiz)

## Postman

Importa `postman/collection.json` en Postman para probar `GET /api/hello`.
Importa `postman/collection.json` en Postman para probar los modulos principales del API.

Cobertura actual de la colección:
- Auth (registro, login, recuperación y logout)
- Users, Roles y Permissions
- Eventos, Inscripciones, Registrations
- Notifications, Paises, Sesiones
- Trabajos cientificos (participante, revisor y comité)
- Mensajes (conversaciones, mensajes, participantes, busqueda y adjuntos)
- SMTP utilitario (`/api/smtp/send`, `/api/smtp/sandbox`)

Variable de entorno usada por Postman:

```text
base_url = http://localhost:8080
```

## Rutas API (Resumen)

Las rutas HTTP se registran en `backend/cmd/api/main.go`. Grupos principales:
- `/api/auth/*`
- `/api/users`, `/api/user/*`, `/api/roles*`, `/api/permissions*`, `/api/resources`
- `/api/eventos*`, `/api/inscripciones*`, `/api/registrations*`
- `/api/notifications*`
- `/api/paises`, `/api/ciudades`, `/api/sesiones*`
- `/api/trabajos-cientificos*`
- `/api/mensajes/*`
- `/api/smtp/*`

## CI (GitHub Actions)

Expand Down
67 changes: 67 additions & 0 deletions TEST_COVERAGE_MATRIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Test Coverage Matrix

Last update: 2026-04-12

## Current Metrics
- Backend Go test files: 17
- Frontend Jest test files: 35
- Frontend Cypress specs: 7

## Newly Covered In This Iteration

### Backend
- Unit: `backend/internal/auth/validation/validation_test.go`
- Unit: `backend/internal/trabajos/validation/validation_test.go`
- Unit: `backend/internal/registrations/validation/validation_test.go`
- Unit: `backend/internal/inscripciones/validation/validation_test.go`
- Unit/HTTP helpers: `backend/internal/shared/response/response_test.go`
- Unit/HTTP helpers: `backend/internal/shared/httperror/httperror_test.go`
- Unit/path helpers: `backend/internal/shared/uploadpath/uploadpath_test.go`
- Unit/role permission helpers: `backend/internal/roles/service/service_test.go`

### Frontend
- Unit/Integration (routing): `frontend/src/navigation/ProtectedRoute.test.tsx`
- Unit/Integration (routing): `frontend/src/navigation/ResourceRoute.test.tsx`
- Unit (route wrapper): `frontend/src/navigation/AdminRoute.test.tsx`
- Unit (screen): `frontend/src/screens/HomeScreen.test.tsx`
- Integration (screen flow): `frontend/src/screens/scientificWorks/ScientificWorksScreen.test.tsx`
- E2E (scientific works history): `frontend/cypress/e2e/dPruebaHistorialTrabajosCientificos.cy.js`
- E2E (protected route): `frontend/cypress/e2e/ePruebaRutaProtegida.cy.js`

## Validation Commands Executed
- `go test ./internal/auth/validation ./internal/trabajos/validation ./internal/registrations/validation ./internal/inscripciones/validation ./internal/shared/response ./internal/shared/httperror ./internal/shared/uploadpath ./internal/roles/service`
- `npm run test -- --runInBand src/navigation/ProtectedRoute.test.tsx src/navigation/ResourceRoute.test.tsx src/navigation/AdminRoute.test.tsx src/screens/HomeScreen.test.tsx`
- `npm run test:e2e -- --spec cypress/e2e/dPruebaHistorialTrabajosCientificos.cy.js`
- `npm run test:e2e -- --spec cypress/e2e/ePruebaRutaProtegida.cy.js`

## Remaining Backlog Snapshot

### Backend high-priority modules still without direct tests
- `backend/internal/events/service/service.go`
- `backend/internal/events/repo/repo.go`
- `backend/internal/auth/service/service.go`
- `backend/internal/auth/service/recovery.go`
- `backend/internal/sesiones/service/service.go`
- `backend/internal/sesiones/handler/handler.go`
- `backend/internal/notifications/service/service.go`
- `backend/internal/notifications/handler/handler.go`
- `backend/internal/trabajos/handler/handler.go`
- `backend/internal/trabajos/repo/repo.go`

### Frontend high-priority modules still without direct tests
- `frontend/src/navigation/AppRouter.tsx`
- `frontend/src/layouts/AppLayout.tsx`
- `frontend/src/contexts/Auth/Authcontext.tsx`
- `frontend/src/components/events/EventDetailModal.tsx`
- `frontend/src/components/notifications/NotificationButton.tsx`
- `frontend/src/screens/events/EventsAdminListScreen.tsx`
- `frontend/src/screens/events/EventsParticipantListScreen.tsx`
- `frontend/src/screens/scientificWorks/ScientificWorksManagementScreen.tsx`
- `frontend/src/screens/auth/LoginScreen.tsx`
- `frontend/src/screens/auth/RegisterScreen.tsx`

## Suggested Closure Order
1. Backend handlers/services with repo mocks (events, sesiones, trabajos, notifications, auth).
2. Frontend contexts and router integration tests (AppRouter + AuthContext + AppLayout).
3. Frontend critical screens not yet covered (events admin/participant, login/register, scientific works management).
4. E2E smoke for role/resource gating paths and scientific works management flow.
13 changes: 12 additions & 1 deletion backend/cmd/api/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package main

// main wires dependencies, registers HTTP routes and starts the API server.

import (
"log"
"net/http"
Expand All @@ -18,6 +20,7 @@ import (
roles "project/backend/internal/roles/service"
sesioneshandler "project/backend/internal/sesiones/handler"
smtphandler "project/backend/internal/shared/smtp"
"project/backend/internal/shared/uploadpath"
userhandler "project/backend/internal/users/handler"
userrepo "project/backend/internal/users/repo"

Expand Down Expand Up @@ -86,9 +89,11 @@ func main() {
trabajosHandler := trabajoshandler.New(prismaClient)
mensajesHandler := mensajeshandler.New(prismaClient)

// User role assignment endpoints.
http.HandleFunc("/api/user/assign-role", userHandler.UpdateUserRoleHandler)
http.HandleFunc("/api/user/assign-roles", userHandler.UpdateUserRolesHandler)

// Authentication and credential recovery endpoints.
http.HandleFunc("/api/auth/register", authHandler.RegisterHandler)
http.HandleFunc("/api/auth/register/request-key", authHandler.RequestRegisterTemporaryKeyHandler)
http.HandleFunc("/api/auth/register/verify-key", authHandler.VerifyRegisterTemporaryKeyHandler)
Expand All @@ -101,6 +106,7 @@ func main() {
http.HandleFunc("/api/smtp/send", smtphandler.SendEmailHandler)
http.HandleFunc("/api/smtp/sandbox", smtphandler.SandboxEmailHandler)

// User, role and permission management endpoints.
http.HandleFunc("/api/hello", userHandler.HelloHandler)
http.HandleFunc("/api/users", userHandler.UsersListHandler)
http.Handle("/api/roles", rolesHandler)
Expand All @@ -110,6 +116,7 @@ func main() {
http.Handle("/api/resources", permissionsHandler)
http.HandleFunc("/api/users/count", userHandler.UsersCountHandler)

// Event and registration ecosystem endpoints.
http.Handle("/api/eventos", eventsHandler)
http.HandleFunc("/api/eventos/fechas-ocupadas", fechasOcupadasHandler)
http.Handle("/api/inscripciones", inscriptionsHandler)
Expand All @@ -124,6 +131,8 @@ func main() {
http.Handle("/api/registrations/", registrationsHandler)
http.Handle("/api/notifications", notificationHandler)
http.Handle("/api/notifications/", notificationHandler)

// Geography, sessions and scientific work endpoints.
http.Handle("/api/paises", paisesHandler)
http.Handle("/api/sesiones", sesionesHandler)
http.Handle("/api/sesiones/", sesionesHandler)
Expand All @@ -135,11 +144,12 @@ func main() {
http.HandleFunc("/api/ciudades", paisHandler.ListCiudadesByPaisHandler)
}

// Messaging and attachment endpoints.
http.Handle("/api/mensajes/conversaciones", mensajesHandler)
http.Handle("/api/mensajes/conversaciones/", mensajesHandler)
http.HandleFunc("/api/mensajes/usuarios/buscar", mensajesHandler.SearchUsuariosHandler)
http.HandleFunc("/api/mensajes/adjuntos", mensajesHandler.UploadAdjuntoHandler)
http.Handle("/uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir("uploads"))))
http.Handle("/uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir(uploadpath.UploadsDir()))))

port := os.Getenv("PORT")
if port == "" {
Expand All @@ -150,6 +160,7 @@ func main() {
log.Fatal(http.ListenAndServe(":"+port, withCORS(http.DefaultServeMux)))
}

// withCORS applies permissive CORS headers used by local frontend and API tooling.
func withCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
Expand Down
47 changes: 47 additions & 0 deletions backend/internal/auth/validation/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package validation

import "testing"

func TestValidateUsername(t *testing.T) {
cases := []struct {
name string
value string
ok bool
}{
{"valid simple", "Juan Perez", true},
{"too short", "Jo", false},
{"has numbers", "Juan1", false},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := ValidateUsername(tc.value); got != tc.ok {
t.Fatalf("ValidateUsername(%q)=%v want %v", tc.value, got, tc.ok)
}
})
}
}

func TestValidateEmail(t *testing.T) {
if !ValidateEmail("user@mail.com") {
t.Fatal("expected valid email")
}
if ValidateEmail("user@mail") {
t.Fatal("expected invalid email")
}
}

func TestValidatePassword(t *testing.T) {
if !ValidatePassword("Abcd1234") {
t.Fatal("expected valid password")
}
if ValidatePassword("abcd1234") {
t.Fatal("expected invalid password without uppercase")
}
if ValidatePassword("ABCD1234") {
t.Fatal("expected invalid password without lowercase")
}
if ValidatePassword("Abcdefgh") {
t.Fatal("expected invalid password without number")
}
}
49 changes: 40 additions & 9 deletions backend/internal/inscripciones/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"context"
"errors"
"fmt"
"strings"
"time"

Expand All @@ -28,6 +29,8 @@ type Service struct {
repo *repo.Repository
}

var sendConfirmationEmailFunc = sendConfirmationEmail

func New(repository *repo.Repository) *Service {
return &Service{repo: repository}
}
Expand All @@ -46,7 +49,8 @@ func (s *Service) CreateInscripcion(ctx context.Context, req dto.CreateInscripci
return 0, ErrEventoCerrado
}

if _, err := s.repo.FindUsuarioByID(ctx, req.IDUsuario); err != nil {
usuario, err := s.repo.FindUsuarioByID(ctx, req.IDUsuario)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
return 0, ErrUsuarioNotFound
}
Expand All @@ -66,11 +70,41 @@ func (s *Service) CreateInscripcion(ctx context.Context, req dto.CreateInscripci

_ = s.repo.InsertHistorial(ctx, id, "", "Pendiente", "Confirmada", "system")
_ = s.repo.InsertNotificacion(ctx, req.IDUsuario, &id, "Confirmación de inscripción", "Tu inscripción fue registrada correctamente")
_ = sendConfirmationEmail(ctx, req.Email, req.NombreParticipante, evento.Nombre)

confirmationEmail := resolveConfirmationEmail(usuario.Email, req.Email)
if err := sendConfirmationEmailWithRetry(ctx, confirmationEmail, req.NombreParticipante, evento.Nombre, 3); err != nil {
fmt.Printf("[ERROR] no se pudo enviar confirmación de inscripción (usuario=%d, inscripcion=%d, email=%s): %v\n", req.IDUsuario, id, confirmationEmail, err)
}

return id, nil
}

func resolveConfirmationEmail(usuarioEmail, requestEmail string) string {
if strings.TrimSpace(usuarioEmail) != "" {
return strings.TrimSpace(usuarioEmail)
}
return strings.TrimSpace(requestEmail)
}

func sendConfirmationEmailWithRetry(ctx context.Context, to, nombre, evento string, maxAttempts int) error {
if maxAttempts < 1 {
maxAttempts = 1
}

var lastErr error
for i := 1; i <= maxAttempts; i++ {
lastErr = sendConfirmationEmailFunc(ctx, to, nombre, evento)
if lastErr == nil {
return nil
}
if i < maxAttempts {
time.Sleep(300 * time.Millisecond)
}
}

return lastErr
}

func sendConfirmationEmail(ctx context.Context, to, nombre, evento string) error {
subject := "Confirmación de inscripción"
body := "Hola " + nombre + ",\n\nTu inscripción al evento '" + evento + "' fue registrada correctamente.\n\nGracias."
Expand Down Expand Up @@ -220,13 +254,10 @@ func (s *Service) UpdateEstado(ctx context.Context, req dto.UpdateEstadoRequest)

_ = s.repo.InsertHistorial(ctx, req.IDInscripcion, actual, newStatus, req.Nota, req.Actor)

pref, err := s.repo.GetPreferencias(ctx, rows[0].IDUsuario)
if err == nil && pref != nil && shouldSendStatusEmail(*pref, actual, newStatus) {
fecha := time.Now().Format("02/01/2006")
asunto, mensaje := buildStatusEmail(rows[0].Nombre, rows[0].EventoNombre, actual, newStatus, fecha, req.Nota)
_ = s.repo.InsertNotificacion(ctx, rows[0].IDUsuario, &req.IDInscripcion, asunto, mensaje)
_ = sendEmail(ctx, rows[0].Email, asunto, mensaje)
}
fecha := time.Now().Format("02/01/2006 15:04")
asunto, mensaje := buildStatusEmail(rows[0].Nombre, rows[0].EventoNombre, actual, newStatus, fecha, req.Nota)
_ = s.repo.InsertNotificacion(ctx, rows[0].IDUsuario, &req.IDInscripcion, asunto, mensaje)
_ = sendEmail(ctx, rows[0].Email, asunto, mensaje)

return nil
}
Expand Down
Loading
Loading