package main import ( "os" "path/filepath" "strings" "testing" "time" "app_compartir/internal/cleaner" "app_compartir/internal/db" "app_compartir/internal/models" "app_compartir/internal/storage" ) func TestGuestExpirationAndRegisteredPermanence(t *testing.T) { tmpDir, err := os.MkdirTemp("", "test_compartir_*") if err != nil { t.Fatalf("error creando tmpDir: %v", err) } defer os.RemoveAll(tmpDir) dbPath := filepath.Join(tmpDir, "test.db") uploadDir := filepath.Join(tmpDir, "uploads") database, err := db.Init(dbPath, uploadDir) if err != nil { t.Fatalf("error inicializando db: %v", err) } defer database.Close() // Crear archivo físico de prueba 1 (Guest vencido) guestExpiredFile := "guest_exp.txt" if err := os.WriteFile(filepath.Join(uploadDir, guestExpiredFile), []byte("contenido guest expirado"), 0644); err != nil { t.Fatalf("error escribiendo archivo guest: %v", err) } // Crear archivo físico de prueba 2 (Usuario Registrado de hace 48 horas) registeredOldFile := "user_old.txt" if err := os.WriteFile(filepath.Join(uploadDir, registeredOldFile), []byte("contenido usuario permanente"), 0644); err != nil { t.Fatalf("error escribiendo archivo usuario: %v", err) } // Crear usuario registrado en BD user := &models.User{ ID: "user_123", Provider: "webcontrol", ProviderID: "123", Email: "pablo@telcotronics.com", Name: "Pablo", CreatedAt: time.Now().Add(-48 * time.Hour), } if err := database.UpsertUser(user); err != nil { t.Fatalf("error creando usuario: %v", err) } // 1. Share de Invitado: expirado (subido hace 25 horas, venció hace 1 hora) pastExp := time.Now().Add(-1 * time.Hour) guestShare := &models.Share{ ID: "guest1", UserID: nil, UserDisplay: "Invitado", IsGuest: true, Type: "file", Title: "guest_doc.txt", StoredPath: guestExpiredFile, FileSize: 23, MimeType: "text/plain", CreatedAt: time.Now().Add(-25 * time.Hour), ExpiresAt: &pastExp, } if err := database.CreateShare(guestShare); err != nil { t.Fatalf("error guardando share guest: %v", err) } // 2. Share de Usuario Registrado: subido hace 48 horas, PERMANENTE (ExpiresAt = nil) regShare := &models.Share{ ID: "reg1", UserID: &user.ID, UserDisplay: user.Name, IsGuest: false, Type: "file", Title: "contrato_permanente.pdf", StoredPath: registeredOldFile, FileSize: 27, MimeType: "application/pdf", CreatedAt: time.Now().Add(-48 * time.Hour), ExpiresAt: nil, // PERMANENTE } if err := database.CreateShare(regShare); err != nil { t.Fatalf("error guardando share registrado: %v", err) } // Ejecutar Cleaner cleanerSvc := cleaner.New(database, uploadDir, 1*time.Minute) // Comprobar items a purgar expiredGuest, err := database.GetExpiredGuestShares() if err != nil { t.Fatalf("error consultando expirados: %v", err) } if len(expiredGuest) != 1 { t.Fatalf("esperado 1 archivo guest expirado, obtenido %d", len(expiredGuest)) } if expiredGuest[0].ID != "guest1" { t.Fatalf("esperado guest1 expirado, obtenido %s", expiredGuest[0].ID) } // Iniciar ciclo de purga cleanerSvc.Start() time.Sleep(100 * time.Millisecond) // esperar a que ejecute runPurge inicial cleanerSvc.Stop() // VERIFICACIÓN 1: El archivo de invitado DEBE haber sido borrado de BD y de disco purgedGuestShare, err := database.GetShare("guest1") if err == nil && purgedGuestShare != nil { t.Fatalf("El archivo de invitado NO debió existir en BD tras la purga") } if _, err := os.Stat(filepath.Join(uploadDir, guestExpiredFile)); !os.IsNotExist(err) { t.Fatalf("El archivo físico de invitado %s debió haber sido eliminado del disco", guestExpiredFile) } // VERIFICACIÓN 2: El archivo del usuario registrado DEBE permanencer intacto (BD y disco) stayedRegShare, err := database.GetShare("reg1") if err != nil || stayedRegShare == nil { t.Fatalf("El archivo de usuario registrado se borró por error: %v", err) } if stayedRegShare.ExpiresAt != nil { t.Fatalf("El archivo registrado no debe tener fecha de caducidad") } if _, err := os.Stat(filepath.Join(uploadDir, registeredOldFile)); os.IsNotExist(err) { t.Fatalf("El archivo físico registrado %s fue borrado erróneamente", registeredOldFile) } t.Log("✅ Test completado con éxito: Solo los datos de invitado caducan en 24h; los de usuarios registrados son permanentes.") } func TestSecurityProtections(t *testing.T) { tmpDir, err := os.MkdirTemp("", "test_security_*") if err != nil { t.Fatalf("error creando tmpDir: %v", err) } defer os.RemoveAll(tmpDir) uploadDir := filepath.Join(tmpDir, "uploads") // 1. Probar Sanitización de Nombres (Path Traversal) traversalTests := []struct { input string expected string }{ {"../../../../etc/passwd", "etc_passwd"}, {"..\\..\\windows\\system32\\cmd.exe", "cmd.exe"}, {"test/../../shell.php", "shell.php"}, {"../../../.htaccess", "_.htaccess"}, } for _, tc := range traversalTests { sanitized := storage.SanitizeFilename(tc.input) if strings.Contains(sanitized, "/") || strings.Contains(sanitized, "\\") || strings.Contains(sanitized, "..") { t.Fatalf("Fallo en sanitización de path traversal para %s: resultado=%s", tc.input, sanitized) } } // 2. Probar Detección de MIME Types Peligrosos (Neutralización de XSS y Scripts) dangerousMimes := []struct { filename string forbidden string expected string }{ {"shell.php", "application/x-httpd-php", "text/plain; charset=utf-8"}, {"exploit.phtml", "text/html", "text/plain; charset=utf-8"}, {"malicious.html", "text/html", "text/plain; charset=utf-8"}, {"test.xhtml", "text/html", "text/plain; charset=utf-8"}, {"script.sh", "application/x-sh", "text/plain; charset=utf-8"}, } for _, tc := range dangerousMimes { m := storage.DetectMimeType(tc.filename, nil) if m == tc.forbidden || strings.HasPrefix(m, "text/html") { t.Fatalf("MIME peligroso no neutralizado para %s: obtenido %s (prohibido %s)", tc.filename, m, tc.forbidden) } if m != tc.expected { t.Fatalf("MIME esperado %s para %s, obtenido %s", tc.expected, tc.filename, m) } } // 3. Probar Guardado Seguro en Disco (Permisos no ejecutables y .htaccess preventivo) sampleContent := strings.NewReader("") storedFile, _, err := storage.SaveFile(uploadDir, "secslug", "shell.php", sampleContent) if err != nil { t.Fatalf("error guardando archivo seguro: %v", err) } // Verificar permisos en disco (NO ejecutable) destPath := filepath.Join(uploadDir, storedFile) fi, err := os.Stat(destPath) if err != nil { t.Fatalf("error comprobando archivo guardado: %v", err) } mode := fi.Mode().Perm() if mode&0111 != 0 { t.Fatalf("El archivo guardado tiene permisos de ejecución: %o", mode) } // Verificar existencia de .htaccess preventivo htaccessPath := filepath.Join(uploadDir, ".htaccess") if _, err := os.Stat(htaccessPath); os.IsNotExist(err) { t.Fatalf(".htaccess preventivo no fue creado en uploadDir") } t.Log("✅ Test completado con éxito: Protecciones de seguridad activas (sin path traversal, scripts neutralizados a texto plano y archivos no ejecutables).") }