212 lines
7.1 KiB
Go
212 lines
7.1 KiB
Go
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("<?php echo 'hacked'; ?>")
|
|
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).")
|
|
}
|