Files
wotra/internal/store/sync_store_test.go
T
2026-08-20 17:35:43 +02:00

168 lines
4.5 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 TestSyncBootstrapAfterPruneReturnsLatestData(t *testing.T) {
s := mustSyncStore(t)
ctx := context.Background()
old := &domain.Entry{ID: "old", DayKey: "2026-01-01", UpdatedAt: time.Now().Add(-31 * 24 * time.Hour).UnixMilli()}
latest := &domain.Entry{ID: "latest", DayKey: "2026-02-01", UpdatedAt: time.Now().UnixMilli()}
if err := s.LogEntry(ctx, old); err != nil {
t.Fatal(err)
}
if err := s.Prune(ctx, -time.Millisecond); err != nil {
t.Fatal(err)
}
if err := s.LogEntry(ctx, latest); err != nil {
t.Fatal(err)
}
// A zero-version client has no history to reconcile. It should receive
// current data rather than being rejected because older history was pruned.
changes, _, err := s.Pull(ctx, 0)
if err != nil {
t.Fatalf("bootstrap should return latest data after pruning, got %v", err)
}
if len(changes) != 1 || changes[0].EntityID != "latest" {
t.Fatalf("expected only latest data, 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)
}
}