add tests

This commit is contained in:
m
2026-09-13 19:24:58 +02:00
parent c054b90c92
commit 5578e4a354
19 changed files with 1599 additions and 1 deletions
+73
View File
@@ -0,0 +1,73 @@
package repository
import (
"testing"
"github.com/vaultdrop/backend/dbtest"
)
func newTestDevices(t *testing.T) *Devices {
t.Helper()
conn := dbtest.OpenTestDatabase(t, repositoryTestURL)
return &Devices{DB: conn}
}
func TestDevicesUpsertAndExists(t *testing.T) {
d := newTestDevices(t)
deviceID := NewID()
exists, err := d.Exists(deviceID)
if err != nil || exists {
t.Fatalf("device non enregistré : exists=%v err=%v", exists, err)
}
if err := d.Upsert(deviceID); err != nil {
t.Fatalf("upsert: %v", err)
}
exists, err = d.Exists(deviceID)
if err != nil || !exists {
t.Fatalf("après upsert : exists=%v err=%v", exists, err)
}
// Upsert idempotent (ON CONFLICT) → pas d'erreur ni de doublon.
if err := d.Upsert(deviceID); err != nil {
t.Fatalf("second upsert: %v", err)
}
exists, err = d.Exists(deviceID)
if err != nil || !exists {
t.Fatalf("après second upsert : exists=%v err=%v", exists, err)
}
var count int
if err := d.DB.QueryRow(`SELECT COUNT(*) FROM devices WHERE device_id = $1`, deviceID).Scan(&count); err != nil {
t.Fatalf("count: %v", err)
}
if count != 1 {
t.Errorf("device dupliqué, count = %d", count)
}
}
func TestDevicesMarkUser(t *testing.T) {
d := newTestDevices(t)
deviceID := NewID()
if err := d.Upsert(deviceID); err != nil {
t.Fatalf("upsert: %v", err)
}
users := &Users{DB: d.DB}
userID, err := users.Create("device-user", "device-user", "hash", false)
if err != nil {
t.Fatalf("create user: %v", err)
}
if err := d.MarkUser(deviceID, userID); err != nil {
t.Fatalf("mark user: %v", err)
}
var storedUser string
if err := d.DB.QueryRow(`SELECT user_id FROM devices WHERE device_id = $1`, deviceID).Scan(&storedUser); err != nil {
t.Fatalf("read user_id: %v", err)
}
if storedUser != userID {
t.Errorf("user_id mémorisé = %q, attendu %q", storedUser, userID)
}
}
+92
View File
@@ -0,0 +1,92 @@
package repository
import (
"errors"
"testing"
"github.com/vaultdrop/backend/dbtest"
)
func newTestRepo(t *testing.T) *Repository {
t.Helper()
conn := dbtest.OpenTestDatabase(t, repositoryTestURL)
return NewRepository(conn)
}
func seedOcrJobFixture(t *testing.T) (*Repository, string, string, string) {
t.Helper()
repo := newTestRepo(t)
userID, err := repo.Users.Create("ocr-user", "ocr-user", "hash", false)
if err != nil {
t.Fatalf("create user: %v", err)
}
deviceID := NewID()
if err := repo.Devices.Upsert(deviceID); err != nil {
t.Fatalf("register device: %v", err)
}
fileID := NewID()
if err := repo.Resources.InsertFile(userID, fileID, "scan.png", "", 10, nil, nil); err != nil {
t.Fatalf("insert file: %v", err)
}
return repo, userID, deviceID, fileID
}
func TestOcrJobsLifecycleTransitions(t *testing.T) {
repo, _, deviceID, fileID := seedOcrJobFixture(t)
jobID := NewID()
if err := repo.OcrJobs.Create(jobID, deviceID, fileID); err != nil {
t.Fatalf("create: %v", err)
}
row, err := repo.OcrJobs.Get(deviceID, jobID)
if err != nil || row.Status != "queued" {
t.Fatalf("get (queued): %+v err=%v", row, err)
}
if err := repo.OcrJobs.TouchProcessing(deviceID, jobID); err != nil {
t.Fatalf("touch processing: %v", err)
}
row, _ = repo.OcrJobs.Get(deviceID, jobID)
if row.Status != "processing" {
t.Errorf("status attendu processing, got %q", row.Status)
}
if err := repo.OcrJobs.Complete(deviceID, jobID, "HELLO"); err != nil {
t.Fatalf("complete: %v", err)
}
row, _ = repo.OcrJobs.Get(deviceID, jobID)
if row.Status != "done" || row.Text == nil || *row.Text != "HELLO" {
t.Errorf("after complete: %+v", row)
}
}
func TestOcrJobsFail(t *testing.T) {
repo, _, deviceID, fileID := seedOcrJobFixture(t)
jobID := NewID()
if err := repo.OcrJobs.Create(jobID, deviceID, fileID); err != nil {
t.Fatalf("create: %v", err)
}
if err := repo.OcrJobs.Fail(deviceID, jobID, "tesseract exploded"); err != nil {
t.Fatalf("fail: %v", err)
}
row, err := repo.OcrJobs.Get(deviceID, jobID)
if err != nil || row.Status != "failed" || row.Error == nil || *row.Error != "tesseract exploded" {
t.Errorf("après fail : %+v err=%v", row, err)
}
}
func TestOcrJobsGetScopedByDevice(t *testing.T) {
repo, _, deviceID, fileID := seedOcrJobFixture(t)
jobID := NewID()
if err := repo.OcrJobs.Create(jobID, deviceID, fileID); err != nil {
t.Fatalf("create: %v", err)
}
otherDevice := NewID()
if err := repo.Devices.Upsert(otherDevice); err != nil {
t.Fatalf("register other device: %v", err)
}
if _, err := repo.OcrJobs.Get(otherDevice, jobID); !errors.Is(err, ErrJobNotFound) {
t.Errorf("get par un autre device : attendu ErrJobNotFound, got %v", err)
}
}
+93
View File
@@ -0,0 +1,93 @@
package repository
import (
"encoding/json"
"errors"
"testing"
"github.com/vaultdrop/backend/dbtest"
)
func newTestOperations(t *testing.T) (*Operations, *Devices) {
t.Helper()
conn := dbtest.OpenTestDatabase(t, repositoryTestURL)
return &Operations{DB: conn}, &Devices{DB: conn}
}
func TestOperationsRecordAndApplied(t *testing.T) {
o, devices := newTestOperations(t)
deviceID := NewID()
if err := devices.Upsert(deviceID); err != nil {
t.Fatalf("register device: %v", err)
}
opID := NewID()
applied, err := o.Applied(deviceID, opID)
if err != nil || applied {
t.Fatalf("avant record : applied=%v err=%v", applied, err)
}
if err := o.Record(deviceID, opID, "create_resource", "resource", nil, NewID(), []byte(`{"name":"x"}`)); err != nil {
t.Fatalf("record: %v", err)
}
applied, err = o.Applied(deviceID, opID)
if err != nil || !applied {
t.Fatalf("après record : applied=%v err=%v", applied, err)
}
// Rejeu → ON CONFLICT DO NOTHING, aucune erreur.
if err := o.Record(deviceID, opID, "create_resource", "resource", nil, NewID(), []byte(`null`)); err != nil {
t.Fatalf("rejeu: %v", err)
}
if applied, _ := o.Applied(deviceID, opID); !applied {
t.Error("rejeu : l'op doit rester appliquée")
}
// Idempotence par (device_id, operation_id) : un autre device peut réutiliser
// le même operation_id sans conflit (les deux rows sont distinctes).
device02 := NewID()
if err := devices.Upsert(device02); err != nil {
t.Fatalf("register device 2: %v", err)
}
if err := o.Record(device02, opID, "create_resource", "resource", nil, NewID(), []byte(`{}`)); err != nil {
t.Errorf("même op_id sur un autre device: %v", err)
}
}
func TestOperationsRecordRejectsInvalidOperationID(t *testing.T) {
o, devices := newTestOperations(t)
if err := devices.Upsert(NewID()); err != nil {
t.Fatalf("register device: %v", err)
}
err := o.Record(NewID(), "UPPERCASENOT32", "create_resource", "resource", nil, NewID(), []byte(`{}`))
if !errors.Is(err, ErrInvalidOperationID) {
t.Errorf("operation_id invalide : attendu ErrInvalidOperationID, got %v", err)
}
}
func TestOperationsRecordNormalizesInvalidPayload(t *testing.T) {
o, devices := newTestOperations(t)
deviceID := NewID()
if err := devices.Upsert(deviceID); err != nil {
t.Fatalf("register device: %v", err)
}
opID := NewID()
// Un payload non-JSON ne doit pas faire échouer la trace (jsonBytes → {}).
if err := o.Record(deviceID, opID, "create_resource", "resource", nil, NewID(), []byte(`not-json`)); err != nil {
t.Fatalf("record: %v", err)
}
var stored string
if err := o.DB.QueryRow(`SELECT payload::text FROM operations WHERE device_id = $1 AND operation_id = $2`, deviceID, opID).Scan(&stored); err != nil {
t.Fatalf("read payload: %v", err)
}
var parsed any
if err := json.Unmarshal([]byte(stored), &parsed); err != nil {
t.Fatalf("payload stocké invalide: %q", stored)
}
if stored != "{}" {
t.Errorf("payload normalisé attendu {}, got %q", stored)
}
}
+1 -1
View File
@@ -305,7 +305,7 @@ func (r *Resources) SyncDelete(ownerID, resourceID string) error {
_, err := r.DB.Exec(
`UPDATE resources SET deleted_at = NOW(), updated_at = NOW()
WHERE resource_id = $1 AND user_id = $2 AND deleted_at IS NULL`,
ownerID, resourceID,
resourceID, ownerID,
)
return err
}
+115
View File
@@ -3,6 +3,7 @@ package repository
import (
"errors"
"testing"
"time"
"github.com/vaultdrop/backend/dbtest"
)
@@ -117,3 +118,117 @@ func TestNameConflictAndUnknownFolder(t *testing.T) {
t.Errorf("folder inconnu doit être NOT_FOUND, got %v", err)
}
}
func TestSearchFilesEscapesWildcards(t *testing.T) {
repo := newTestResources(t)
owner := NewID()
mustInsertUser(t, repo, owner)
insert := func(name string) {
t.Helper()
if err := repo.InsertFile(owner, NewID(), name, "", 1, nil, nil); err != nil {
t.Fatalf("insert %q: %v", name, err)
}
}
insert("half%price.txt")
insert("plain.txt")
insert("a_b.txt")
insert("abx.txt")
insert(`win\file.txt`)
// '%' littéral → uniquement le nom contenant un '%'
files, total, err := repo.SearchFiles(owner, "%", 50, 0)
if err != nil {
t.Fatalf("search %%: %v", err)
}
if total != 1 || len(files) != 1 || files[0].Name != "half%price.txt" {
t.Errorf("'%%' littéral : total=%d files=%+v", total, files)
}
// '_' littéral → uniquement a_b.txt (pas abx.txt)
files, total, err = repo.SearchFiles(owner, "_", 50, 0)
if err != nil {
t.Fatalf("search _: %v", err)
}
if total != 1 || files[0].Name != "a_b.txt" {
t.Errorf("'_' littéral : total=%d files=%+v", total, files)
}
// '\' littéral → uniquement win\file.txt
files, total, err = repo.SearchFiles(owner, `win\file`, 50, 0)
if err != nil {
t.Fatalf("search backslash: %v", err)
}
if total != 1 || files[0].Name != `win\file.txt` {
t.Errorf("'\\' littéral : total=%d files=%+v", total, files)
}
}
func TestListFilesPagination(t *testing.T) {
repo := newTestResources(t)
owner := NewID()
mustInsertUser(t, repo, owner)
for _, name := range []string{"a.txt", "b.txt", "c.txt"} {
if err := repo.InsertFile(owner, NewID(), name, "", 1, nil, nil); err != nil {
t.Fatalf("insert %q: %v", name, err)
}
}
files, total, err := repo.ListFiles(owner, "", 2, 0, "name", "asc")
if err != nil {
t.Fatalf("list page 1: %v", err)
}
if total != 3 || len(files) != 2 || files[0].Name != "a.txt" || files[1].Name != "b.txt" {
t.Errorf("page 1 : total=%d files=%+v", total, files)
}
files, total, err = repo.ListFiles(owner, "", 2, 2, "name", "asc")
if err != nil {
t.Fatalf("list page 2: %v", err)
}
if total != 3 || len(files) != 1 || files[0].Name != "c.txt" {
t.Errorf("page 2 : total=%d files=%+v", total, files)
}
}
func TestListOwnedDelta(t *testing.T) {
repo := newTestResources(t)
owner := NewID()
mustInsertUser(t, repo, owner)
if err := repo.InsertFolder(owner, NewID(), "Docs", ""); err != nil {
t.Fatalf("insert folder: %v", err)
}
if err := repo.InsertFile(owner, NewID(), "old.txt", "", 1, nil, nil); err != nil {
t.Fatalf("insert old: %v", err)
}
// aprèsMs=0 → tout
all, err := repo.ListOwned(owner, 0)
if err != nil || len(all) != 2 {
t.Fatalf("snapshot complet: %+v err=%v", all, err)
}
// aprèsMs dans le futur → vide
far, err := repo.ListOwned(owner, time.Now().Add(time.Hour).UnixMilli())
if err != nil || len(far) != 0 {
t.Errorf("aprèsMs futur : attendu vide, got %+v err=%v", far, err)
}
// Delta : une ressource insérée APRÈS baseline → uniquement celle-là.
baseline := time.Now()
time.Sleep(5 * time.Millisecond)
freshID := NewID()
if err := repo.InsertFile(owner, freshID, "fresh.txt", "", 1, nil, nil); err != nil {
t.Fatalf("insert fresh: %v", err)
}
delta, err := repo.ListOwned(owner, baseline.UnixMilli())
if err != nil {
t.Fatalf("delta: %v", err)
}
if len(delta) != 1 || delta[0].ID != freshID {
t.Errorf("delta attendu uniquement fresh.txt, got %+v", delta)
}
}
+88
View File
@@ -0,0 +1,88 @@
package repository
import (
"errors"
"testing"
"github.com/vaultdrop/backend/dbtest"
)
func newTestUsers(t *testing.T) *Users {
t.Helper()
conn := dbtest.OpenTestDatabase(t, repositoryTestURL)
return &Users{DB: conn}
}
func TestUsersCreateGetAndCount(t *testing.T) {
u := newTestUsers(t)
id, err := u.Create("Alice", "alice", "hash-1", false)
if err != nil {
t.Fatalf("create: %v", err)
}
row, err := u.GetByUsernameNormalized("alice")
if err != nil {
t.Fatalf("get by normalized: %v", err)
}
if row.ID != id || row.Username != "Alice" || row.UsernameNormalized != "alice" || row.IsAdmin || row.PasswordHash != "hash-1" {
t.Errorf("row inattendue: %+v", row)
}
byID, err := u.GetByID(id)
if err != nil || byID.ID != id {
t.Fatalf("get by id: %+v err=%v", byID, err)
}
resolved, err := u.ResolveExact("alice")
if err != nil || resolved.ID != id {
t.Fatalf("resolve exact: %+v err=%v", resolved, err)
}
n, err := u.Count()
if err != nil || n != 1 {
t.Errorf("count = %d err=%v, attendu 1", n, err)
}
// Doublon de username normalisé → ErrNameConflict.
if _, err := u.Create("alice2", "alice", "hash-X", false); !errors.Is(err, ErrNameConflict) {
t.Errorf("doublon normalisé : attendu ErrNameConflict, got %v", err)
}
}
func TestUsersUpdatePassword(t *testing.T) {
u := newTestUsers(t)
id, err := u.Create("bob", "bob", "hash-1", false)
if err != nil {
t.Fatalf("create: %v", err)
}
if err := u.UpdatePassword(id, "hash-2"); err != nil {
t.Fatalf("update password: %v", err)
}
row, err := u.GetByID(id)
if err != nil || row.PasswordHash != "hash-2" {
t.Errorf("hash après update = %q (err=%v)", row.PasswordHash, err)
}
}
func TestUsersMarkDeletedHidesAccount(t *testing.T) {
u := newTestUsers(t)
id, err := u.Create("carol", "carol", "hash-1", false)
if err != nil {
t.Fatalf("create: %v", err)
}
if err := u.MarkDeleted(id); err != nil {
t.Fatalf("mark deleted: %v", err)
}
if _, err := u.GetByID(id); !errors.Is(err, ErrNotFound) {
t.Errorf("get by id après suppression : attendu ErrNotFound, got %v", err)
}
if _, err := u.GetByUsernameNormalized("carol"); !errors.Is(err, ErrNotFound) {
t.Errorf("get par username après suppression : attendu ErrNotFound, got %v", err)
}
if n, _ := u.Count(); n != 1 {
t.Errorf("count doit compter aussi les supprimés (bootstrap), got %d", n)
}
}