owpengram-server/internal/rpc/durable_layer_evidence_test.go
2026-09-01 12:06:31 +03:00

273 lines
13 KiB
Go

package rpc
import (
"context"
"errors"
"testing"
"time"
"github.com/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/proto"
"go.uber.org/zap/zaptest"
"telesrv/internal/domain"
"telesrv/internal/store"
"telesrv/internal/store/memory"
)
func TestDurableSessionLayerSurvivesRestartAndRejectsOldSelectorRollback(t *testing.T) {
ctx := context.Background()
keys := memory.NewAuthKeyStore()
authKeyID := [8]byte{1}
if err := keys.Save(ctx, store.AuthKeyData{ID: authKeyID}); err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
olderID := int64(proto.NewMessageIDGen(func() time.Time { return now.Add(-time.Second) }).New(proto.MessageFromClient))
newerID := int64(proto.NewMessageIDGen(func() time.Time { return now }).New(proto.MessageFromClient))
first := New(Config{}, Deps{AuthKeySessionLayers: keys}, zaptest.NewLogger(t), clock.System)
if layer, msgID, publish, err := first.AdvanceNegotiatedSessionLayerEvidence(ctx, authKeyID, 10, 225, olderID); err != nil || layer != 225 || msgID != olderID || !publish {
t.Fatalf("first evidence = (%d,%d,%v,%v)", layer, msgID, publish, err)
}
if layer, msgID, publish, err := first.AdvanceNegotiatedSessionLayerEvidence(ctx, authKeyID, 10, 227, newerID); err != nil || layer != 227 || msgID != newerID || !publish {
t.Fatalf("newer evidence = (%d,%d,%v,%v)", layer, msgID, publish, err)
}
// A fresh Router has no process-local registry. The durable raw-session
// watermark restores the exact codec before a naked same-session replay.
restarted := New(Config{}, Deps{AuthKeySessionLayers: keys}, zaptest.NewLogger(t), clock.System)
layer, msgID, found, err := restarted.ResolveNegotiatedSessionLayerEvidence(ctx, authKeyID, 10)
if err != nil || !found || layer != 227 || msgID != newerID {
t.Fatalf("restart restore = (%d,%d,%v,%v)", layer, msgID, found, err)
}
layer, msgID, publish, err := restarted.AdvanceNegotiatedSessionLayerEvidence(ctx, authKeyID, 10, 225, olderID)
if err != nil || layer != 227 || msgID != newerID || publish {
t.Fatalf("old selector after restart = (%d,%d,%v,%v)", layer, msgID, publish, err)
}
key, found, err := keys.Get(ctx, authKeyID)
if err != nil || !found || key.Layer != 227 {
t.Fatalf("durable default rolled back: key=%+v found=%v err=%v", key, found, err)
}
}
func TestDurableSessionLayerSameGenerationSkipsSharedPublication(t *testing.T) {
ctx := context.Background()
keys := memory.NewAuthKeyStore()
authKeyID := [8]byte{1, 0xb3}
if err := keys.Save(ctx, store.AuthKeyData{ID: authKeyID}); err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
msgIDs := proto.NewMessageIDGen(func() time.Time { return now })
firstID := int64(msgIDs.New(proto.MessageFromClient))
secondID := int64(msgIDs.New(proto.MessageFromClient))
r := New(Config{}, Deps{AuthKeySessionLayers: keys}, zaptest.NewLogger(t), clock.System)
if layer, msgID, publish, err := r.AdvanceNegotiatedSessionLayerEvidence(ctx, authKeyID, 11, 227, firstID); err != nil || layer != 227 || msgID != firstID || !publish {
t.Fatalf("first generation = (%d,%d,%v,%v)", layer, msgID, publish, err)
}
first, found, err := keys.GetSessionLayer(ctx, authKeyID, 11)
if err != nil || !found {
t.Fatalf("first row = (%+v,%v,%v)", first, found, err)
}
if layer, msgID, publish, err := r.AdvanceNegotiatedSessionLayerEvidence(ctx, authKeyID, 11, 227, secondID); err != nil || layer != 227 || msgID != secondID || publish {
t.Fatalf("same generation = (%d,%d,%v,%v)", layer, msgID, publish, err)
}
second, found, err := keys.GetSessionLayer(ctx, authKeyID, 11)
if err != nil || !found || second.ObservationID != first.ObservationID {
t.Fatalf("same-generation row = (%+v,%v,%v), first observation %d", second, found, err, first.ObservationID)
}
}
func TestDurableSessionLayerResolveRefreshesStaleRouterFromSharedStore(t *testing.T) {
ctx := context.Background()
keys := memory.NewAuthKeyStore()
authKeyID := [8]byte{1, 0xa2}
const sessionID = int64(102)
if err := keys.Save(ctx, store.AuthKeyData{ID: authKeyID}); err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
msgIDs := proto.NewMessageIDGen(func() time.Time { return now })
oldMsgID := msgIDs.New(proto.MessageFromClient)
newMsgID := msgIDs.New(proto.MessageFromClient)
routerA := New(Config{}, Deps{AuthKeySessionLayers: keys}, zaptest.NewLogger(t), clock.System)
routerB := New(Config{}, Deps{AuthKeySessionLayers: keys}, zaptest.NewLogger(t), clock.System)
if layer, msgID, _, err := routerA.AdvanceNegotiatedSessionLayerEvidence(ctx, authKeyID, sessionID, 225, oldMsgID); err != nil || layer != 225 || msgID != oldMsgID {
t.Fatalf("A advance = (%d,%d,%v)", layer, msgID, err)
}
if layer, msgID, found, err := routerA.ResolveNegotiatedSessionLayerEvidence(ctx, authKeyID, sessionID); err != nil || !found || layer != 225 || msgID != oldMsgID {
t.Fatalf("A initial resolve = (%d,%d,%v,%v)", layer, msgID, found, err)
}
if layer, msgID, _, err := routerB.AdvanceNegotiatedSessionLayerEvidence(ctx, authKeyID, sessionID, 227, newMsgID); err != nil || layer != 227 || msgID != newMsgID {
t.Fatalf("B advance = (%d,%d,%v)", layer, msgID, err)
}
// A still has 225 in its process-local accelerator. Durable-store mode must
// read primary first and converge both the return value and that accelerator.
if layer, msgID, found, err := routerA.ResolveNegotiatedSessionLayerEvidence(ctx, authKeyID, sessionID); err != nil || !found || layer != 227 || msgID != newMsgID {
t.Fatalf("A refreshed resolve = (%d,%d,%v,%v), want B's 227", layer, msgID, found, err)
}
if layer, msgID, found := routerA.NegotiatedSessionLayerEvidence(authKeyID, sessionID); !found || layer != 227 || msgID != newMsgID {
t.Fatalf("A refreshed local cache = (%d,%d,%v)", layer, msgID, found)
}
}
func TestDurableSessionLayerFutureProfileCanBeCorrectedByGreaterSelector(t *testing.T) {
ctx := context.Background()
keys := memory.NewAuthKeyStore()
authKeyID := [8]byte{2}
if err := keys.Save(ctx, store.AuthKeyData{ID: authKeyID}); err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
futureID := int64(proto.NewMessageIDGen(func() time.Time { return now }).New(proto.MessageFromClient))
correctID := int64(proto.NewMessageIDGen(func() time.Time { return now.Add(time.Second) }).New(proto.MessageFromClient))
if _, applied, err := keys.AdvanceSessionLayer(ctx, authKeyID, 20, 230, futureID); err != nil || !applied {
t.Fatalf("seed future evidence = applied %v err %v", applied, err)
}
r := New(Config{}, Deps{AuthKeySessionLayers: keys}, zaptest.NewLogger(t), clock.System)
layer, msgID, found, err := r.ResolveNegotiatedSessionLayerEvidence(ctx, authKeyID, 20)
if err != nil || !found || layer != 230 || msgID != futureID {
t.Fatalf("future restore = (%d,%d,%v,%v)", layer, msgID, found, err)
}
if _, _, cached := r.NegotiatedSessionLayerEvidence(authKeyID, 20); cached {
t.Fatal("unsupported future profile polluted typed process cache")
}
layer, msgID, publish, err := r.AdvanceNegotiatedSessionLayerEvidence(ctx, authKeyID, 20, 228, correctID)
if err != nil || layer != 228 || msgID != correctID || !publish {
t.Fatalf("future self-heal = (%d,%d,%v,%v)", layer, msgID, publish, err)
}
}
func TestDurableSessionLayerExpiryCoversAcceptedFutureSkew(t *testing.T) {
ctx := context.Background()
keys := memory.NewAuthKeyStore()
authKeyID := [8]byte{3}
if err := keys.Save(ctx, store.AuthKeyData{ID: authKeyID}); err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
msgID := int64(proto.NewMessageIDGen(func() time.Time { return now.Add(29 * time.Second) }).New(proto.MessageFromClient))
r := New(Config{}, Deps{AuthKeySessionLayers: keys}, zaptest.NewLogger(t), clock.System)
if _, _, _, err := r.AdvanceNegotiatedSessionLayerEvidence(ctx, authKeyID, 30, 227, msgID); err != nil {
t.Fatal(err)
}
value, found, err := keys.GetSessionLayer(ctx, authKeyID, 30)
if err != nil || !found {
t.Fatalf("future-skew evidence = found %v err %v", found, err)
}
wantMin := proto.MessageID(msgID).Time().Add(300 * time.Second)
if value.ExpiresAt.Before(wantMin) {
t.Fatalf("future-skew expiry = %v, want >= %v", value.ExpiresAt, wantMin)
}
}
type unavailableSessionLayerStore struct {
store.AuthKeySessionLayerStore
err error
}
func (s unavailableSessionLayerStore) GetSessionLayer(context.Context, [8]byte, int64) (store.AuthKeySessionLayer, bool, error) {
return store.AuthKeySessionLayer{}, false, s.err
}
type mutableSessionLayerStore struct {
store.AuthKeySessionLayerStore
value store.AuthKeySessionLayer
}
func (s *mutableSessionLayerStore) GetSessionLayer(context.Context, [8]byte, int64) (store.AuthKeySessionLayer, bool, error) {
return s.value, true, nil
}
func TestDurableSessionLayerCacheOrdersRebuiltRowsByObservationID(t *testing.T) {
now := time.Now().UTC()
primary := &mutableSessionLayerStore{value: store.AuthKeySessionLayer{
Layer: 227, MessageID: 10_000, ObservationID: 10, ExpiresAt: now.Add(time.Hour),
}}
r := New(Config{}, Deps{AuthKeySessionLayers: primary}, zaptest.NewLogger(t), clock.System)
authKeyID := [8]byte{3, 0xb5}
const sessionID = int64(305)
if layer, msgID, found, err := r.ResolveNegotiatedSessionLayerEvidence(context.Background(), authKeyID, sessionID); err != nil || !found || layer != 227 || msgID != 10_000 {
t.Fatalf("first row = (%d,%d,%v,%v)", layer, msgID, found, err)
}
// An expired durable row may be rebuilt with a lower client msg_id. The
// globally increasing observation id, not msg_id, orders those row lifetimes.
primary.value = store.AuthKeySessionLayer{
Layer: 225, MessageID: 100, ObservationID: 11, ExpiresAt: now.Add(time.Hour),
}
if layer, msgID, found, err := r.ResolveNegotiatedSessionLayerEvidence(context.Background(), authKeyID, sessionID); err != nil || !found || layer != 225 || msgID != 100 {
t.Fatalf("rebuilt row = (%d,%d,%v,%v)", layer, msgID, found, err)
}
if layer, msgID, found := r.NegotiatedSessionLayerEvidence(authKeyID, sessionID); !found || layer != 225 || msgID != 100 {
t.Fatalf("rebuilt local cache = (%d,%d,%v)", layer, msgID, found)
}
}
func TestDurableSessionLayerCacheAdvancesHighWaterWithinObservation(t *testing.T) {
now := time.Now().UTC()
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
authKeyID := [8]byte{3, 0xc6}
const sessionID = int64(306)
first := store.AuthKeySessionLayer{
Layer: 227, MessageID: 10_000, ObservationID: 10, ExpiresAt: now.Add(time.Minute),
}
if err := r.cacheResolvedDurableSessionLayer(authKeyID, sessionID, first); err != nil {
t.Fatal(err)
}
advanced := first
advanced.MessageID = 20_000
advanced.ExpiresAt = now.Add(2 * time.Minute)
if err := r.cacheResolvedDurableSessionLayer(authKeyID, sessionID, advanced); err != nil {
t.Fatal(err)
}
if layer, msgID, found := r.NegotiatedSessionLayerEvidence(authKeyID, sessionID); !found || layer != 227 || msgID != 20_000 {
t.Fatalf("same-observation advance = (%d,%d,%v)", layer, msgID, found)
}
stale := first
stale.MessageID = 15_000
if err := r.cacheResolvedDurableSessionLayer(authKeyID, sessionID, stale); err != nil {
t.Fatal(err)
}
if layer, msgID, found := r.NegotiatedSessionLayerEvidence(authKeyID, sessionID); !found || layer != 227 || msgID != 20_000 {
t.Fatalf("stale same-observation read rolled cache back = (%d,%d,%v)", layer, msgID, found)
}
}
func TestDurableSessionLayerAvailabilityErrorCarriesStructuralMarker(t *testing.T) {
boom := errors.New("database unavailable")
r := New(Config{}, Deps{AuthKeySessionLayers: unavailableSessionLayerStore{err: boom}}, zaptest.NewLogger(t), clock.System)
_, _, _, err := r.ResolveNegotiatedSessionLayerEvidence(context.Background(), [8]byte{4}, 40)
var marker interface{ LayerEvidenceDurabilityUnavailable() }
if !errors.Is(err, boom) || !errors.As(err, &marker) {
t.Fatalf("availability error = %v, marker=%v", err, marker != nil)
}
}
func TestDurableInheritedLayerRevalidatesEachNewSession(t *testing.T) {
ctx := context.Background()
authKeyID := [8]byte{5}
keys := memory.NewAuthKeyStore()
if err := keys.Save(ctx, store.AuthKeyData{ID: authKeyID}); err != nil {
t.Fatal(err)
}
auth := &captureAuthService{authKeyClientInfos: map[[8]byte]domain.AuthKeyClientInfo{
authKeyID: {Layer: 225, LayerObservationID: 1},
}}
r := New(Config{}, Deps{Auth: auth, AuthKeySessionLayers: keys}, zaptest.NewLogger(t), clock.System)
if layer, found, err := r.ResolveInheritedAuthKeyLayer(ctx, authKeyID); err != nil || !found || layer != 225 {
t.Fatalf("initial default = (%d,%v,%v)", layer, found, err)
}
auth.authKeyClientInfos[authKeyID] = domain.AuthKeyClientInfo{Layer: 230, LayerObservationID: 2}
if layer, found, err := r.ResolveInheritedAuthKeyLayer(ctx, authKeyID); err != nil || !found || layer != 0 {
t.Fatalf("future authoritative default = (%d,%v,%v)", layer, found, err)
}
auth.authKeyClientInfos[authKeyID] = domain.AuthKeyClientInfo{Layer: 228, LayerObservationID: 3}
if layer, found, err := r.ResolveInheritedAuthKeyLayer(ctx, authKeyID); err != nil || !found || layer != 228 {
t.Fatalf("corrected default = (%d,%v,%v)", layer, found, err)
}
if auth.authKeyInfoLookups != 3 {
t.Fatalf("auth_keys lookups = %d, want one per new session resolution", auth.authKeyInfoLookups)
}
}