add tests
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/vaultdrop/backend/pkg/passwd"
|
||||
"github.com/vaultdrop/backend/repository"
|
||||
)
|
||||
|
||||
func TestEnsureAdminCreatesFirstAdmin(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
repo := s.Repository
|
||||
|
||||
if err := EnsureAdmin(repo, " Admin ", "admin-secret-123"); err != nil {
|
||||
t.Fatalf("EnsureAdmin: %v", err)
|
||||
}
|
||||
|
||||
user, err := repo.Users.GetByUsernameNormalized("admin")
|
||||
if err != nil {
|
||||
t.Fatalf("admin non trouvé: %v", err)
|
||||
}
|
||||
if !user.IsAdmin {
|
||||
t.Error("le compte créé doit être admin")
|
||||
}
|
||||
if user.UsernameNormalized != "admin" || user.Username != "admin" {
|
||||
t.Errorf("identité anormale: %+v", user)
|
||||
}
|
||||
if err := passwd.Verify("admin-secret-123", user.PasswordHash); err != nil {
|
||||
t.Errorf("le mot de passe doit vérifier: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeUsername(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
" admin ": "admin",
|
||||
" Alice": "alice",
|
||||
"A B C": "a b c",
|
||||
"": "",
|
||||
" ": "",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := NormalizeUsername(in); got != want {
|
||||
t.Errorf("NormalizeUsername(%q) = %q, attendu %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureAdminRefusesOnEmptyDBWithoutEnv(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
|
||||
err := EnsureAdmin(s.Repository, "", "")
|
||||
if !errors.Is(err, ErrAdminRequired) {
|
||||
t.Errorf("env absent : attendu ErrAdminRequired, got %v", err)
|
||||
}
|
||||
|
||||
err = EnsureAdmin(s.Repository, " ", "some-password")
|
||||
if !errors.Is(err, ErrAdminRequired) {
|
||||
t.Errorf("username blanc : attendu ErrAdminRequired, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureAdminNeverOverwritesExistingAccounts(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
repo := s.Repository
|
||||
|
||||
// La base n'est PAS vide → l'env ne doit rien créer, même avec des valeurs.
|
||||
if _, err := repo.Users.Create("alice", "alice", "existing-hash", false); err != nil {
|
||||
t.Fatalf("seed user: %v", err)
|
||||
}
|
||||
|
||||
if err := EnsureAdmin(repo, "ADMIN", "would-be-admin-secret"); err != nil {
|
||||
t.Fatalf("EnsureAdmin sur base non vide : %v", err)
|
||||
}
|
||||
|
||||
// Aucun admin n'a été ajouté, l'existant est intact.
|
||||
if _, err := repo.Users.GetByUsernameNormalized("admin"); !errors.Is(err, repository.ErrNotFound) {
|
||||
t.Errorf("admin ne doit pas exister sur base non vide (err=%v)", err)
|
||||
}
|
||||
alice, err := repo.Users.GetByUsernameNormalized("alice")
|
||||
if err != nil {
|
||||
t.Fatalf("alice doit être intacte: %v", err)
|
||||
}
|
||||
if alice.PasswordHash != "existing-hash" || alice.IsAdmin {
|
||||
t.Errorf("l'existant doit être préservé: %+v", alice)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vaultdrop/backend/ocr"
|
||||
"github.com/vaultdrop/backend/repository"
|
||||
)
|
||||
|
||||
// stubEngine again (package service) — même contrat que handlers/ocr_test.
|
||||
type stubEngine struct {
|
||||
text string
|
||||
err error
|
||||
}
|
||||
|
||||
func (s stubEngine) ExtractText(_ context.Context, _ string, _ string) (string, error) {
|
||||
if s.err != nil {
|
||||
return "", s.err
|
||||
}
|
||||
return s.text, nil
|
||||
}
|
||||
|
||||
// newTestOcr builds an Ocr over a fresh DB and returns it with the ids.
|
||||
func newTestOcr(t *testing.T, uploadDir string, engine ocr.Engine) (*Ocr, string, string, string) {
|
||||
t.Helper()
|
||||
s := newServiceStore(t)
|
||||
userID := mustCreateUser(t, s.Repository, "ocr-failed")
|
||||
deviceID := mustRegisterDevice(t, s.Repository, repository.NewID())
|
||||
return NewOcr(s.Repository, uploadDir, "fra+eng", engine), userID, deviceID, uploadDir
|
||||
}
|
||||
|
||||
// waitTillTerminal poll jusqu'à un statut terminal (done/failed).
|
||||
func waitTillTerminal(t *testing.T, o *Ocr, deviceID, jobID string) repository.OcrJobRow {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
row, err := o.Repository.OcrJobs.Get(deviceID, jobID)
|
||||
if err != nil {
|
||||
t.Fatalf("get job: %v", err)
|
||||
}
|
||||
if row.Status == "done" || row.Status == "failed" {
|
||||
return row
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
t.Fatal("job jamais terminal")
|
||||
return repository.OcrJobRow{}
|
||||
}
|
||||
|
||||
func insertOcrFile(t *testing.T, o *Ocr, userID, fileID string) {
|
||||
t.Helper()
|
||||
if err := o.Repository.Resources.InsertFile(userID, fileID, "scan.png", "", 128, nil, nil); err != nil {
|
||||
t.Fatalf("insert file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOcrPhysicalPath(t *testing.T) {
|
||||
uploadDir := t.TempDir()
|
||||
userID := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
fileID := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(uploadDir, userID), 0o755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
// Vérifie que l'extension du fichier trouvé est acceptée (glob = ordre
|
||||
// alphabétique, on ne présume pas du nom choisi).
|
||||
if err := os.WriteFile(filepath.Join(uploadDir, userID, fileID+".png"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatalf("write png: %v", err)
|
||||
}
|
||||
|
||||
o := NewOcr(nil, uploadDir, "fra+eng", nil)
|
||||
|
||||
found, err := o.physicalPath(userID, fileID)
|
||||
if err != nil {
|
||||
t.Fatalf("physicalPath: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(filepath.Base(found), fileID+".") {
|
||||
t.Errorf("path = %q, attendu préfixe %q", found, fileID+".")
|
||||
}
|
||||
|
||||
if _, err := o.physicalPath(userID, "cccccccccccccccccccccccccccccccc"); !errors.Is(err, repository.ErrNotFound) {
|
||||
t.Errorf("fichier absent : attendu ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOcrJobFailsWhenPhysicalFileMissing(t *testing.T) {
|
||||
o, userID, deviceID, _ := newTestOcr(t, t.TempDir(), stubEngine{text: "x"})
|
||||
|
||||
fileID := repository.NewID()
|
||||
insertOcrFile(t, o, userID, fileID)
|
||||
|
||||
job, err := o.Create(userID, deviceID, fileID)
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
row := waitTillTerminal(t, o, deviceID, job.ID)
|
||||
if row.Status != "failed" || row.Error == nil {
|
||||
t.Errorf("fichier physique absent : attendu failed avec erreur, got %+v", row)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOcrJobFailsOnEngineError(t *testing.T) {
|
||||
uploadDir := t.TempDir()
|
||||
o, userID, deviceID, _ := newTestOcr(t, uploadDir, stubEngine{err: errors.New("tesseract boom")})
|
||||
|
||||
fileID := repository.NewID()
|
||||
insertOcrFile(t, o, userID, fileID)
|
||||
// Le fichier physique doit exister pour atteindre l'engine.
|
||||
if err := os.MkdirAll(filepath.Join(uploadDir, userID), 0o755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(uploadDir, userID, fileID+".png"), []byte("img"), 0o644); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
|
||||
job, err := o.Create(userID, deviceID, fileID)
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
row := waitTillTerminal(t, o, deviceID, job.ID)
|
||||
if row.Status != "failed" || row.Error == nil || !strings.Contains(*row.Error, "tesseract boom") {
|
||||
t.Errorf("erreur engine : attendu failed avec message engine, got %+v", row)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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/<user>/<id>.<ext>.
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/vaultdrop/backend/dbtest"
|
||||
"github.com/vaultdrop/backend/repository"
|
||||
)
|
||||
|
||||
const serviceTestURL = "postgres://vaultdrop:vaultdrop@localhost:5432/vaultdrop_service_test?sslmode=disable"
|
||||
|
||||
// newServiceStore returns a Resources service over a fresh migrated test DB.
|
||||
func newServiceStore(t *testing.T) *Resources {
|
||||
t.Helper()
|
||||
conn := dbtest.OpenTestDatabase(t, serviceTestURL)
|
||||
repo := repository.NewRepository(conn)
|
||||
return NewResources(repo, t.TempDir(), 100)
|
||||
}
|
||||
|
||||
// mustCreateUser creates a plain (non-admin) account and returns its id.
|
||||
func mustCreateUser(t *testing.T, repo *repository.Repository, username string) string {
|
||||
t.Helper()
|
||||
id, err := repo.Users.Create(username, username, "test-hash", false)
|
||||
if err != nil {
|
||||
t.Fatalf("create user %q: %v", username, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// mustRegisterDevice registers a device (idempotent) and returns its id.
|
||||
func mustRegisterDevice(t *testing.T, repo *repository.Repository, deviceID string) string {
|
||||
t.Helper()
|
||||
if err := repo.Devices.Upsert(deviceID); err != nil {
|
||||
t.Fatalf("register device: %v", err)
|
||||
}
|
||||
return deviceID
|
||||
}
|
||||
|
||||
func strp(v string) *string { return &v }
|
||||
|
||||
// syncOp builds a SyncOperation with a payload serialized as JSON.
|
||||
func syncOp(operationID, resourceID, operation, resourceType string, payload any) SyncOperation {
|
||||
raw, _ := json.Marshal(payload)
|
||||
return SyncOperation{
|
||||
OperationID: operationID,
|
||||
ResourceID: strp(resourceID),
|
||||
ResourceType: strp(resourceType),
|
||||
Operation: operation,
|
||||
Payload: raw,
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSyncOp(t *testing.T) {
|
||||
hexID := repository.NewID()
|
||||
|
||||
valid := syncOp(hexID, hexID, OpCreateResource, "file", map[string]any{"name": "x.txt"})
|
||||
if err := validateSyncOp(&valid); err != nil {
|
||||
t.Errorf("op valide rejetée: %v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
op SyncOperation
|
||||
}{
|
||||
{"operation_id non 32-hex", syncOp("UPPERCASE", hexID, OpCreateResource, "file", map[string]any{"name": "x"})},
|
||||
{"operation_id trop court", syncOp("abc", hexID, OpCreateResource, "file", map[string]any{"name": "x"})},
|
||||
{"operation vide", func() SyncOperation {
|
||||
o := syncOp(hexID, hexID, OpCreateResource, "file", map[string]any{"name": "x"})
|
||||
o.Operation = ""
|
||||
return o
|
||||
}()},
|
||||
{"operation inconnue", syncOp(hexID, hexID, "explode", "file", map[string]any{"name": "x"})},
|
||||
{"resource_id non 32-hex", syncOp(hexID, "not-hex", OpCreateResource, "file", map[string]any{"name": "x"})},
|
||||
{"resource_type invalide", syncOp(hexID, hexID, OpCreateResource, "document", map[string]any{"name": "x"})},
|
||||
{"resource_type manquant", func() SyncOperation {
|
||||
o := syncOp(hexID, hexID, OpCreateResource, "file", map[string]any{"name": "x"})
|
||||
o.ResourceType = nil
|
||||
return o
|
||||
}()},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if err := validateSyncOp(&tc.op); err == nil {
|
||||
t.Errorf("%s: validation attendue à échouer", tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSyncOpAckOnlyOpsSkipResourceValidation(t *testing.T) {
|
||||
// Les ops partage/lien sont accusées réception SANS état serveur : ni
|
||||
// resource_id ni resource_type ne sont exigés.
|
||||
for _, opType := range []string{OpShare, OpRevokeShare, OpUpdateShare, OpCreateLink, OpRevokeLink} {
|
||||
op := SyncOperation{
|
||||
OperationID: repository.NewID(),
|
||||
Operation: opType,
|
||||
Payload: json.RawMessage(`{}`),
|
||||
}
|
||||
if err := validateSyncOp(&op); err != nil {
|
||||
t.Errorf("%s: ack-only op rejetée: %v", opType, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBatchRejectsInvalidOperationID(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
userID := mustCreateUser(t, s.Repository, "sync-invalid")
|
||||
deviceID := mustRegisterDevice(t, s.Repository, repository.NewID())
|
||||
|
||||
result, err := s.ApplyBatch(userID, deviceID, []SyncOperation{
|
||||
syncOp("NOT-32-HEX", repository.NewID(), OpCreateResource, "file", map[string]any{"name": "x.txt"}),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyBatch: %v", err)
|
||||
}
|
||||
if result.Applied != 0 || result.Failed == nil {
|
||||
t.Fatalf("attendu applied=0 + échec, got %+v", result)
|
||||
}
|
||||
if result.Failed.OperationID != "NOT-32-HEX" || result.Failed.Code != "INVALID_REQUEST" {
|
||||
t.Errorf("failed inattendu: %+v", result.Failed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBatchAckOnlyOpsRecordedWithoutState(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
userID := mustCreateUser(t, s.Repository, "sync-ack")
|
||||
deviceID := mustRegisterDevice(t, s.Repository, repository.NewID())
|
||||
|
||||
ops := []SyncOperation{
|
||||
syncOp(repository.NewID(), repository.NewID(), OpShare, "file", map[string]any{}),
|
||||
syncOp(repository.NewID(), repository.NewID(), OpCreateLink, "file", map[string]any{}),
|
||||
}
|
||||
result, err := s.ApplyBatch(userID, deviceID, ops)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyBatch: %v", err)
|
||||
}
|
||||
if result.Applied != 2 || result.Failed != nil {
|
||||
t.Errorf("attendu applied=2, got %+v", result)
|
||||
}
|
||||
// Aucune ressource ne doit avoir été créée.
|
||||
files, total, _ := s.Repo.ListFiles(userID, "", 10, 0, "created_at", "desc")
|
||||
if len(files) != 0 || total != 0 {
|
||||
t.Errorf("aucune ressource attendue, got %d", total)
|
||||
}
|
||||
|
||||
// Rejeu → toujours accusé, jamais visible.
|
||||
result, err = s.ApplyBatch(userID, deviceID, ops)
|
||||
if err != nil || result.Applied != 2 {
|
||||
t.Errorf("rejeu: applied=%d err=%v", result.Applied, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBatchUpdateMetadata(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
userID := mustCreateUser(t, s.Repository, "sync-rename")
|
||||
deviceID := mustRegisterDevice(t, s.Repository, repository.NewID())
|
||||
|
||||
fileID := repository.NewID()
|
||||
ops := []SyncOperation{
|
||||
syncOp(repository.NewID(), fileID, OpCreateResource, "file", map[string]any{"name": "before.txt"}),
|
||||
syncOp(repository.NewID(), fileID, OpUpdateMetadata, "file", map[string]any{"name": "after.txt"}),
|
||||
}
|
||||
result, err := s.ApplyBatch(userID, deviceID, ops)
|
||||
if err != nil || result.Applied != 2 || result.Failed != nil {
|
||||
t.Fatalf("ApplyBatch: applied=%d failed=%+v err=%v", result.Applied, result.Failed, err)
|
||||
}
|
||||
row, err := s.Repo.GetFile(userID, fileID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetFile: %v", err)
|
||||
}
|
||||
if row.Name != "after.txt" {
|
||||
t.Errorf("nom après update_metadata = %q, attendu after.txt", row.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBatchMoveIntoItselfRejected(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
userID := mustCreateUser(t, s.Repository, "sync-selfmove")
|
||||
deviceID := mustRegisterDevice(t, s.Repository, repository.NewID())
|
||||
|
||||
fileID := repository.NewID()
|
||||
if _, err := s.ApplyBatch(userID, deviceID, []SyncOperation{
|
||||
syncOp(repository.NewID(), fileID, OpCreateResource, "file", map[string]any{"name": "x.txt"}),
|
||||
}); err != nil {
|
||||
t.Fatalf("apply create: %v", err)
|
||||
}
|
||||
|
||||
result, err := s.ApplyBatch(userID, deviceID, []SyncOperation{
|
||||
syncOp(repository.NewID(), fileID, OpMoveResource, "file", map[string]any{"toFolderResourceId": fileID}),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyBatch: %v", err)
|
||||
}
|
||||
if result.Applied != 0 || result.Failed == nil || result.Failed.Code != "INVALID_REQUEST" {
|
||||
t.Errorf("move dans soi-même: attendu applied=0 INVALID_REQUEST, got %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBatchMoveToMissingFolder(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
userID := mustCreateUser(t, s.Repository, "sync-move-missing")
|
||||
deviceID := mustRegisterDevice(t, s.Repository, repository.NewID())
|
||||
|
||||
fileID := repository.NewID()
|
||||
if _, err := s.ApplyBatch(userID, deviceID, []SyncOperation{
|
||||
syncOp(repository.NewID(), fileID, OpCreateResource, "file", map[string]any{"name": "x.txt"}),
|
||||
}); err != nil {
|
||||
t.Fatalf("apply create: %v", err)
|
||||
}
|
||||
|
||||
missing := repository.NewID()
|
||||
result, err := s.ApplyBatch(userID, deviceID, []SyncOperation{
|
||||
syncOp(repository.NewID(), fileID, OpMoveResource, "file", map[string]any{"toFolderResourceId": missing}),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyBatch: %v", err)
|
||||
}
|
||||
if result.Applied != 0 || result.Failed == nil || result.Failed.Code != "NOT_FOUND" {
|
||||
t.Errorf("move vers dossier absent: attendu applied=0 NOT_FOUND, got %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBatchRejectsInvalidPayloadJSON(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
userID := mustCreateUser(t, s.Repository, "sync-badpayload")
|
||||
deviceID := mustRegisterDevice(t, s.Repository, repository.NewID())
|
||||
|
||||
op := syncOp(repository.NewID(), repository.NewID(), OpCreateResource, "file", map[string]any{})
|
||||
op.Payload = json.RawMessage(`{"name":`)
|
||||
|
||||
result, err := s.ApplyBatch(userID, deviceID, []SyncOperation{op})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyBatch: %v", err)
|
||||
}
|
||||
if result.Applied != 0 || result.Failed == nil || result.Failed.Code != "INVALID_REQUEST" {
|
||||
t.Errorf("payload JSON invalide: attendu applied=0 INVALID_REQUEST, got %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBatchRejectsMissingName(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
userID := mustCreateUser(t, s.Repository, "sync-noname")
|
||||
deviceID := mustRegisterDevice(t, s.Repository, repository.NewID())
|
||||
|
||||
result, err := s.ApplyBatch(userID, deviceID, []SyncOperation{
|
||||
syncOp(repository.NewID(), repository.NewID(), OpCreateResource, "file", map[string]any{}),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyBatch: %v", err)
|
||||
}
|
||||
if result.Applied != 0 || result.Failed == nil || result.Failed.Code != "INVALID_REQUEST" {
|
||||
t.Errorf("create sans name: attendu applied=0 INVALID_REQUEST, got %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBatchDeleteIsIdempotent(t *testing.T) {
|
||||
s := newServiceStore(t)
|
||||
userID := mustCreateUser(t, s.Repository, "sync-del")
|
||||
deviceID := mustRegisterDevice(t, s.Repository, repository.NewID())
|
||||
|
||||
fileID := repository.NewID()
|
||||
ops := []SyncOperation{
|
||||
syncOp(repository.NewID(), fileID, OpCreateResource, "file", map[string]any{"name": "x.txt"}),
|
||||
syncOp(repository.NewID(), fileID, OpDeleteResource, "file", map[string]any{}),
|
||||
}
|
||||
result, err := s.ApplyBatch(userID, deviceID, ops)
|
||||
if err != nil || result.Applied != 2 || result.Failed != nil {
|
||||
t.Fatalf("ApplyBatch: applied=%d failed=%+v err=%v", result.Applied, result.Failed, err)
|
||||
}
|
||||
if _, err := s.Repo.GetFile(userID, fileID); !errors.Is(err, repository.ErrNotFound) {
|
||||
t.Errorf("fichier supprimé attendu NOT_FOUND, got %v", err)
|
||||
}
|
||||
|
||||
// Suppression d'une ressource déjà absente → no-op réussi.
|
||||
result, err = s.ApplyBatch(userID, deviceID, []SyncOperation{
|
||||
syncOp(repository.NewID(), fileID, OpDeleteResource, "file", map[string]any{}),
|
||||
})
|
||||
if err != nil || result.Applied != 1 || result.Failed != nil {
|
||||
t.Errorf("delete absent: applied=%d failed=%+v err=%v", result.Applied, result.Failed, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user