add tests
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user