161 lines
4.4 KiB
Go
161 lines
4.4 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/wotra/wotra/internal/domain"
|
|
"github.com/wotra/wotra/internal/store"
|
|
)
|
|
|
|
func mustSyncStore(t *testing.T) *store.SyncStore {
|
|
t.Helper()
|
|
db, err := store.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
return store.NewSyncStore(db)
|
|
}
|
|
|
|
func TestSyncPullNormal(t *testing.T) {
|
|
s := mustSyncStore(t)
|
|
ctx := context.Background()
|
|
|
|
e1 := &domain.Entry{ID: "e1", DayKey: "2026-04-01", UpdatedAt: time.Now().UnixMilli()}
|
|
e2 := &domain.Entry{ID: "e2", DayKey: "2026-04-02", UpdatedAt: time.Now().UnixMilli()}
|
|
if err := s.LogEntry(ctx, e1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.LogEntry(ctx, e2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
changes, ver, err := s.Pull(ctx, 0)
|
|
if err != nil {
|
|
t.Fatalf("Pull: %v", err)
|
|
}
|
|
if len(changes) != 2 {
|
|
t.Fatalf("expected 2 changes, got %d", len(changes))
|
|
}
|
|
if ver != 2 {
|
|
t.Fatalf("expected server_version=2, got %d", ver)
|
|
}
|
|
|
|
// Incremental pull: since=1 should return only e2.
|
|
changes2, ver2, err := s.Pull(ctx, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(changes2) != 1 || changes2[0].EntityID != "e2" {
|
|
t.Fatalf("expected [e2], got %+v", changes2)
|
|
}
|
|
if ver2 != 2 {
|
|
t.Fatalf("expected ver=2, got %d", ver2)
|
|
}
|
|
}
|
|
|
|
func TestSyncPruneStaleClient(t *testing.T) {
|
|
s := mustSyncStore(t)
|
|
ctx := context.Background()
|
|
|
|
// Log two entries then prune all of them (zero TTL).
|
|
e1 := &domain.Entry{ID: "e1", DayKey: "2026-01-01", UpdatedAt: time.Now().UnixMilli()}
|
|
e2 := &domain.Entry{ID: "e2", DayKey: "2026-01-02", UpdatedAt: time.Now().UnixMilli()}
|
|
if err := s.LogEntry(ctx, e1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.LogEntry(ctx, e2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Prune with -1ms TTL → cutoff is 1ms in the future, so all rows are pruned.
|
|
if err := s.Prune(ctx, -time.Millisecond); err != nil {
|
|
t.Fatalf("Prune: %v", err)
|
|
}
|
|
|
|
// A client with an existing sync position behind the marker is stale.
|
|
_, _, err := s.Pull(ctx, 1)
|
|
if !errors.Is(err, store.ErrSyncStale) {
|
|
t.Fatalf("expected ErrSyncStale, got %v", err)
|
|
}
|
|
}
|
|
|
|
|
|
func TestSyncBootstrapIncludesPrunedEntityData(t *testing.T) {
|
|
db, err := store.Open(":memory:")
|
|
if err != nil { t.Fatal(err) }
|
|
defer db.Close()
|
|
|
|
ctx := context.Background()
|
|
entries := store.NewEntryStore(db)
|
|
entry := &domain.Entry{ID: "retained", StartTime: 1, DayKey: "2026-01-01", Note: "old"}
|
|
if err := entries.Create(ctx, entry); err != nil { t.Fatal(err) }
|
|
syncStore := store.NewSyncStore(db)
|
|
if err := syncStore.Prune(ctx, -time.Millisecond); err != nil { t.Fatal(err) }
|
|
|
|
changes, version, err := syncStore.Bootstrap(ctx)
|
|
if err != nil { t.Fatal(err) }
|
|
if version != 0 { t.Fatalf("expected sync version 0, got %d", version) }
|
|
if len(changes) == 0 || changes[0].Entity != "entries" || changes[0].EntityID != entry.ID {
|
|
t.Fatalf("expected current entry in bootstrap, got %+v", changes)
|
|
}
|
|
}
|
|
func TestSyncPruneNoRows(t *testing.T) {
|
|
s := mustSyncStore(t)
|
|
ctx := context.Background()
|
|
|
|
// Prune on empty log is a no-op.
|
|
if err := s.Prune(ctx, 30*24*time.Hour); err != nil {
|
|
t.Fatalf("Prune on empty log: %v", err)
|
|
}
|
|
|
|
changes, ver, err := s.Pull(ctx, 0)
|
|
if err != nil {
|
|
t.Fatalf("Pull: %v", err)
|
|
}
|
|
if len(changes) != 0 {
|
|
t.Fatalf("expected 0 changes, got %d", len(changes))
|
|
}
|
|
if ver != 0 {
|
|
t.Fatalf("expected ver=0, got %d", ver)
|
|
}
|
|
}
|
|
|
|
func TestSyncClientAheadOfMarker(t *testing.T) {
|
|
s := mustSyncStore(t)
|
|
ctx := context.Background()
|
|
|
|
// Log two entries, prune all, then log a third.
|
|
e1 := &domain.Entry{ID: "e1", DayKey: "2026-01-01", UpdatedAt: time.Now().UnixMilli()}
|
|
e2 := &domain.Entry{ID: "e2", DayKey: "2026-01-02", UpdatedAt: time.Now().UnixMilli()}
|
|
if err := s.LogEntry(ctx, e1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.LogEntry(ctx, e2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.Prune(ctx, -time.Millisecond); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Marker is at version 2. Log a new entry → version 3.
|
|
e3 := &domain.Entry{ID: "e3", DayKey: "2026-04-01", UpdatedAt: time.Now().UnixMilli()}
|
|
if err := s.LogEntry(ctx, e3); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// A client with since=2 is past the marker — should get only e3.
|
|
changes, ver, err := s.Pull(ctx, 2)
|
|
if err != nil {
|
|
t.Fatalf("expected no error for up-to-date client, got %v", err)
|
|
}
|
|
if len(changes) != 1 || changes[0].EntityID != "e3" {
|
|
t.Fatalf("expected [e3], got %+v", changes)
|
|
}
|
|
if ver != 3 {
|
|
t.Fatalf("expected ver=3, got %d", ver)
|
|
}
|
|
}
|