Ctrl K
ComunidadGuías…

internal/app/userprojection/durable_user_facts_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package userprojection

import (
	"context"
	"errors"
	"sync"
	"testing"

	"telesrv/internal/app/readmodel"
	"telesrv/internal/domain"
	"telesrv/internal/store"
)

type durableFactVersions struct {
	mu     sync.Mutex
	hashes map[store.ReadModelKey]int64
}

func (v *durableFactVersions) ReadModelHash(_ context.Context, model string, ownerUserID int64, peerType domain.PeerType, peerID int64) (int64, bool, error) {
	v.mu.Lock()
	defer v.mu.Unlock()
	hash := v.hashes[store.ReadModelKey{Model: model, OwnerUserID: ownerUserID, PeerType: peerType, PeerID: peerID}]
	return hash, hash != 0, nil
}

func (v *durableFactVersions) ReadModelHashes(_ context.Context, keys []store.ReadModelKey) (map[store.ReadModelKey]int64, error) {
	v.mu.Lock()
	defer v.mu.Unlock()
	out := make(map[store.ReadModelKey]int64, len(keys))
	for _, key := range keys {
		if hash := v.hashes[key]; hash != 0 {
			out[key] = hash
		}
	}
	return out, nil
}

func (v *durableFactVersions) set(model string, userID, hash int64) {
	v.mu.Lock()
	v.hashes[store.ReadModelKey{Model: model, OwnerUserID: 0, PeerType: domain.PeerTypeUser, PeerID: userID}] = hash
	v.mu.Unlock()
}

type countingDurableFreezeFacts struct {
	mu     sync.Mutex
	calls  int
	values map[int64]domain.AccountFreeze
	err    error
}

func (f *countingDurableFreezeFacts) AccountFreezes(_ context.Context, ids []int64) (map[int64]domain.AccountFreeze, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.calls++
	if f.err != nil {
		return nil, f.err
	}
	out := make(map[int64]domain.AccountFreeze)
	for _, id := range ids {
		if value, ok := f.values[id]; ok {
			out[id] = value
		}
	}
	return out, nil
}

type countingDurablePhoneFacts struct {
	mu     sync.Mutex
	calls  int
	values map[int64]domain.CollectiblePhone
	err    error
}

func (f *countingDurablePhoneFacts) OwnedCollectiblePhones(_ context.Context, ids []int64) (map[int64]domain.CollectiblePhone, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.calls++
	if f.err != nil {
		return nil, f.err
	}
	out := make(map[int64]domain.CollectiblePhone)
	for _, id := range ids {
		if value, ok := f.values[id]; ok {
			out[id] = value
		}
	}
	return out, nil
}

func TestDurableUserProjectionFactsCachesPositiveAndNegativeByVersion(t *testing.T) {
	ctx := context.Background()
	versions := &durableFactVersions{hashes: make(map[store.ReadModelKey]int64)}
	for _, id := range []int64{1, 2} {
		versions.set(readmodel.ModelUserVisibility, id, 10+id)
		versions.set(readmodel.ModelUserCollectiblePhone, id, 20+id)
	}
	freezes := &countingDurableFreezeFacts{values: map[int64]domain.AccountFreeze{1: {UserID: 1, Frozen: true}}}
	phones := &countingDurablePhoneFacts{values: map[int64]domain.CollectiblePhone{1: {OwnerUserID: 1, Phone: "8881"}}}
	facts := NewDurableUserProjectionFacts(freezes, phones, versions, 10)

	for i := 0; i < 2; i++ {
		gotFreezes, err := facts.AccountFreezes(ctx, []int64{1, 2, 1})
		if err != nil || len(gotFreezes) != 1 || !gotFreezes[1].Frozen {
			t.Fatalf("AccountFreezes(%d) = %+v err=%v", i, gotFreezes, err)
		}
		gotPhones, err := facts.OwnedCollectiblePhones(ctx, []int64{1, 2, 1})
		if err != nil || len(gotPhones) != 1 || gotPhones[1].Phone != "8881" {
			t.Fatalf("OwnedCollectiblePhones(%d) = %+v err=%v", i, gotPhones, err)
		}
	}
	if freezes.calls != 1 || phones.calls != 1 {
		t.Fatalf("backend calls freezes/phones = %d/%d, want 1/1 including negative hits", freezes.calls, phones.calls)
	}

	versions.set(readmodel.ModelUserVisibility, 2, 32)
	versions.set(readmodel.ModelUserCollectiblePhone, 2, 42)
	freezes.values[2] = domain.AccountFreeze{UserID: 2, Frozen: true}
	phones.values[2] = domain.CollectiblePhone{OwnerUserID: 2, Phone: "8882"}
	gotFreezes, err := facts.AccountFreezes(ctx, []int64{1, 2})
	if err != nil || !gotFreezes[2].Frozen {
		t.Fatalf("AccountFreezes after version bump = %+v err=%v", gotFreezes, err)
	}
	gotPhones, err := facts.OwnedCollectiblePhones(ctx, []int64{1, 2})
	if err != nil || gotPhones[2].Phone != "8882" {
		t.Fatalf("OwnedCollectiblePhones after version bump = %+v err=%v", gotPhones, err)
	}
	if freezes.calls != 2 || phones.calls != 2 {
		t.Fatalf("backend calls after one-key bumps freezes/phones = %d/%d, want 2/2", freezes.calls, phones.calls)
	}
}

func TestDurableUserProjectionFactsScalarFreezeGateReusesVersionedFact(t *testing.T) {
	ctx := context.Background()
	versions := &durableFactVersions{hashes: make(map[store.ReadModelKey]int64)}
	versions.set(readmodel.ModelUserVisibility, 1, 11)
	versions.set(readmodel.ModelUserVisibility, 2, 12)
	freezes := &countingDurableFreezeFacts{values: map[int64]domain.AccountFreeze{
		1: {UserID: 1, Frozen: true},
	}}
	facts := NewDurableUserProjectionFacts(freezes, nil, versions, 10)

	for i := 0; i < 3; i++ {
		freeze, found, err := facts.AccountFreeze(ctx, 1)
		if err != nil || !found || !freeze.Frozen {
			t.Fatalf("positive scalar gate %d = %+v found=%v err=%v", i, freeze, found, err)
		}
		freeze, found, err = facts.AccountFreeze(ctx, 2)
		if err != nil || found || freeze.Frozen {
			t.Fatalf("negative scalar gate %d = %+v found=%v err=%v", i, freeze, found, err)
		}
	}
	if freezes.calls != 2 {
		t.Fatalf("backend calls = %d, want one positive and one negative fill", freezes.calls)
	}
}

func TestDurableUserProjectionFactErrorsAreNotNegativeCached(t *testing.T) {
	ctx := context.Background()
	versions := &durableFactVersions{hashes: make(map[store.ReadModelKey]int64)}
	versions.set(readmodel.ModelUserVisibility, 1, 11)
	versions.set(readmodel.ModelUserCollectiblePhone, 1, 21)
	freezes := &countingDurableFreezeFacts{values: map[int64]domain.AccountFreeze{}, err: errors.New("freeze unavailable")}
	phones := &countingDurablePhoneFacts{values: map[int64]domain.CollectiblePhone{}, err: errors.New("phone unavailable")}
	facts := NewDurableUserProjectionFacts(freezes, phones, versions, 10)

	if _, err := facts.AccountFreezes(ctx, []int64{1}); err == nil {
		t.Fatal("AccountFreezes error = nil")
	}
	if _, err := facts.OwnedCollectiblePhones(ctx, []int64{1}); err == nil {
		t.Fatal("OwnedCollectiblePhones error = nil")
	}
	freezes.err = nil
	phones.err = nil
	if _, err := facts.AccountFreezes(ctx, []int64{1}); err != nil {
		t.Fatalf("recovered AccountFreezes: %v", err)
	}
	if _, err := facts.OwnedCollectiblePhones(ctx, []int64{1}); err != nil {
		t.Fatalf("recovered OwnedCollectiblePhones: %v", err)
	}
	if freezes.calls != 2 || phones.calls != 2 {
		t.Fatalf("backend calls freezes/phones = %d/%d, want retry", freezes.calls, phones.calls)
	}
}

func TestDurableUserProjectionFactExplicitInvalidation(t *testing.T) {
	ctx := context.Background()
	versions := &durableFactVersions{hashes: make(map[store.ReadModelKey]int64)}
	versions.set(readmodel.ModelUserVisibility, 1, 11)
	versions.set(readmodel.ModelUserCollectiblePhone, 1, 21)
	freezes := &countingDurableFreezeFacts{values: map[int64]domain.AccountFreeze{}}
	phones := &countingDurablePhoneFacts{values: map[int64]domain.CollectiblePhone{}}
	facts := NewDurableUserProjectionFacts(freezes, phones, versions, 10)
	_, _ = facts.AccountFreezes(ctx, []int64{1})
	_, _ = facts.OwnedCollectiblePhones(ctx, []int64{1})
	facts.InvalidateAccountFreezeFact(1)
	facts.InvalidateCollectiblePhoneFact(1)
	_, _ = facts.AccountFreezes(ctx, []int64{1})
	_, _ = facts.OwnedCollectiblePhones(ctx, []int64{1})
	if freezes.calls != 2 || phones.calls != 2 {
		t.Fatalf("backend calls after invalidation freezes/phones = %d/%d, want 2/2", freezes.calls, phones.calls)
	}
}