package service import ( "bytes" "errors" "mime/multipart" "os" "path/filepath" "testing" "github.com/vaultdrop/backend/repository" ) func multipartFileHeader(t *testing.T, filename string, content []byte) *multipart.FileHeader { t.Helper() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", filename) if err != nil { t.Fatalf("create form: %v", err) } if _, err := part.Write(content); err != nil { t.Fatalf("write: %v", err) } if err := writer.Close(); err != nil { t.Fatalf("close: %v", err) } reader := multipart.NewReader(&body, writer.Boundary()) form, err := reader.ReadForm(1 << 20) if err != nil { t.Fatalf("read form: %v", err) } files := form.File["file"] if len(files) == 0 { t.Fatal("aucun fichier dans la forme") } return files[0] } func TestUploadPersistsPhysicalFile(t *testing.T) { s := newServiceStore(t) userID := mustCreateUser(t, s.Repository, "upload-happy") dto, err := s.Upload(userID, multipartFileHeader(t, "docs.txt", []byte("hello")), "", "") if err != nil { t.Fatalf("upload: %v", err) } if dto.ID == "" || dto.Name != "docs.txt" { t.Errorf("dto inattendu: %+v", dto) } // Le fichier physique existe sous UploadDir//.. path := filepath.Join(s.UploadDir, userID, dto.ID+".txt") info, err := os.Stat(path) if err != nil { t.Fatalf("fichier physique absent: %v", err) } if info.Size() != 5 { t.Errorf("taille physique = %d, attendu 5", info.Size()) } } func TestUploadRemovesPhysicalFileOnNameConflict(t *testing.T) { s := newServiceStore(t) userID := mustCreateUser(t, s.Repository, "upload-conflict") folderID := repository.NewID() if err := s.Repository.Resources.InsertFolder(userID, folderID, "Docs", ""); err != nil { t.Fatalf("insert folder: %v", err) } target := multipartFileHeader(t, "dupe.txt", []byte("hello")) first, err := s.Upload(userID, target, folderID, "") if err != nil { t.Fatalf("premier upload: %v", err) } if first.ID == "" { t.Fatal("aucun id au premier upload") } // Même nom, même dossier → NAME_CONFLICT et pas de second fichier physique. if _, err := s.Upload(userID, target, folderID, ""); !errors.Is(err, repository.ErrNameConflict) { t.Fatalf("second upload : attendu ErrNameConflict, got %v", err) } entries, err := os.ReadDir(filepath.Join(s.UploadDir, userID)) if err != nil { t.Fatalf("readdir: %v", err) } if len(entries) != 1 { t.Errorf("fichier orphelin laissé après NAME_CONFLICT : %d fichiers", len(entries)) } } func TestUploadTargetsExistingResource(t *testing.T) { s := newServiceStore(t) userID := mustCreateUser(t, s.Repository, "upload-target") // Fiche metadata-only créée par l'outbox côté client. resourceID := repository.NewID() ext := "png" if err := s.Repository.Resources.InsertFile(userID, resourceID, "scan.png", "", 0, nil, &ext); err != nil { t.Fatalf("insert metadata-only file: %v", err) } // Re-upload ciblé : mêmes octets, même resource_id, pas de doublon. dto, err := s.Upload(userID, multipartFileHeader(t, "scan.png", []byte("PNGDATA")), "", resourceID) if err != nil { t.Fatalf("targeted upload: %v", err) } if dto.ID != resourceID { t.Errorf("id attendu %s, got %s", resourceID, dto.ID) } if dto.Size != 7 { t.Errorf("taille = %d, attendu 7 (rafraîchie)", dto.Size) } path := filepath.Join(s.UploadDir, userID, resourceID+".png") info, err := os.Stat(path) if err != nil { t.Fatalf("fichier physique absent: %v", err) } if info.Size() != 7 { t.Errorf("taille physique = %d, attendu 7", info.Size()) } // Une seule ligne serveur (pas de doublon créé par le re-upload). rows, total, err := s.Repo.ListFiles(userID, "", 50, 0, "", "") if err != nil { t.Fatalf("list: %v", err) } if total != 1 || len(rows) != 1 || rows[0].ID != resourceID { t.Errorf("liste après re-upload: total=%d rows=%+v", total, rows) } } func TestUploadTargetsOwnedResourceOnly(t *testing.T) { s := newServiceStore(t) userID := mustCreateUser(t, s.Repository, "upload-owner") otherUser := mustCreateUser(t, s.Repository, "upload-owner-other") resourceID := repository.NewID() if err := s.Repository.Resources.InsertFile(otherUser, resourceID, "mine.png", "", 0, nil, nil); err != nil { t.Fatalf("insert other's file: %v", err) } // Un resource_id d'un autre user → ErrNotFound, aucun octet écrit. if _, err := s.Upload(userID, multipartFileHeader(t, "mine.png", []byte("DATA")), "", resourceID); !errors.Is(err, repository.ErrNotFound) { t.Fatalf("upload cross-user : attendu ErrNotFound, got %v", err) } // Le dossier upload du user ne doit pas avoir été créé (échec avant écriture). if _, err := os.Stat(filepath.Join(s.UploadDir, userID)); !os.IsNotExist(err) { t.Errorf("dossier upload créé à tort ou erreur inattendue: %v", err) } } func TestUploadTargetUnknownIsNotFound(t *testing.T) { s := newServiceStore(t) userID := mustCreateUser(t, s.Repository, "upload-unknown") if _, err := s.Upload(userID, multipartFileHeader(t, "ghost.png", []byte("DATA")), "", repository.NewID()); !errors.Is(err, repository.ErrNotFound) { t.Fatalf("upload inconnu : attendu ErrNotFound, got %v", err) } }