owpengram-server/internal/app/privacy/cache_test.go

355 lines
11 KiB
Go

package privacy
import (
"context"
"sync"
"testing"
"time"
"telesrv/internal/domain"
"telesrv/internal/store"
"telesrv/internal/store/memory"
)
type blockingFirstPrivacyStore struct {
store.PrivacyStore
started chan struct{}
release chan struct{}
first []domain.PrivacyRules
mu sync.Mutex
firstUsed bool
}
func (s *blockingFirstPrivacyStore) ListPrivacyRules(ctx context.Context, ownerUserIDs []int64, keys []domain.PrivacyKey) ([]domain.PrivacyRules, error) {
s.mu.Lock()
if !s.firstUsed {
s.firstUsed = true
s.mu.Unlock()
close(s.started)
select {
case <-s.release:
case <-ctx.Done():
return nil, ctx.Err()
}
out := make([]domain.PrivacyRules, len(s.first))
for i := range s.first {
out[i] = cloneRules(s.first[i])
}
return out, nil
}
s.mu.Unlock()
return s.PrivacyStore.ListPrivacyRules(ctx, ownerUserIDs, keys)
}
func waitForPrivacyCacheTestSignal(t *testing.T, ch <-chan struct{}) {
t.Helper()
select {
case <-ch:
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for cache test signal")
}
}
type countingPrivacyStore struct {
store.PrivacyStore
getCalls int
listCalls int
setCalls int
}
func (s *countingPrivacyStore) GetPrivacyRules(ctx context.Context, ownerUserID int64, key domain.PrivacyKey) (domain.PrivacyRules, bool, error) {
s.getCalls++
return s.PrivacyStore.GetPrivacyRules(ctx, ownerUserID, key)
}
func (s *countingPrivacyStore) SetPrivacyRules(ctx context.Context, rules domain.PrivacyRules) error {
s.setCalls++
return s.PrivacyStore.SetPrivacyRules(ctx, rules)
}
func (s *countingPrivacyStore) ListPrivacyRules(ctx context.Context, ownerUserIDs []int64, keys []domain.PrivacyKey) ([]domain.PrivacyRules, error) {
s.listCalls++
return s.PrivacyStore.ListPrivacyRules(ctx, ownerUserIDs, keys)
}
func TestCachedPrivacyStoreUsesOwnerSnapshot(t *testing.T) {
ctx := context.Background()
base := memory.NewPrivacyStore()
if err := base.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyPhoneNumber,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleDisallowAll}},
}); err != nil {
t.Fatalf("seed privacy: %v", err)
}
counting := &countingPrivacyStore{PrivacyStore: base}
cached := NewCachedPrivacyStore(counting, 0)
first, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber)
if err != nil || !ok {
t.Fatalf("first get ok=%v err=%v", ok, err)
}
if first.Rules[0].Kind != domain.PrivacyRuleDisallowAll {
t.Fatalf("first rules = %+v, want disallow all", first.Rules)
}
second, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber)
if err != nil || !ok {
t.Fatalf("second get ok=%v err=%v", ok, err)
}
if second.Rules[0].Kind != domain.PrivacyRuleDisallowAll {
t.Fatalf("second rules = %+v, want disallow all", second.Rules)
}
if counting.getCalls != 0 {
t.Fatalf("GetPrivacyRules calls = %d, want 0", counting.getCalls)
}
if counting.listCalls != 1 {
t.Fatalf("ListPrivacyRules calls = %d, want 1 owner snapshot load", counting.listCalls)
}
}
func TestCachedPrivacyStoreInvalidatesOnSet(t *testing.T) {
ctx := context.Background()
base := memory.NewPrivacyStore()
counting := &countingPrivacyStore{PrivacyStore: base}
cached := NewCachedPrivacyStore(counting, 0)
if err := cached.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyPhoneNumber,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleDisallowAll}},
}); err != nil {
t.Fatalf("set first: %v", err)
}
if _, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber); err != nil || !ok {
t.Fatalf("prime get ok=%v err=%v", ok, err)
}
if err := cached.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyPhoneNumber,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleAllowAll}},
}); err != nil {
t.Fatalf("set second: %v", err)
}
got, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber)
if err != nil || !ok {
t.Fatalf("after invalidation get ok=%v err=%v", ok, err)
}
if got.Rules[0].Kind != domain.PrivacyRuleAllowAll {
t.Fatalf("rules after invalidation = %+v, want allow all", got.Rules)
}
if counting.listCalls != 2 {
t.Fatalf("ListPrivacyRules calls = %d, want 2 after invalidation", counting.listCalls)
}
}
func TestCachedPrivacyStoreExternalInvalidationAndFlush(t *testing.T) {
ctx := context.Background()
base := memory.NewPrivacyStore()
if err := base.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyPhoneNumber,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleDisallowAll}},
}); err != nil {
t.Fatalf("seed privacy: %v", err)
}
counting := &countingPrivacyStore{PrivacyStore: base}
cached := NewCachedPrivacyStore(counting, 0)
if _, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber); err != nil || !ok {
t.Fatalf("prime get ok=%v err=%v", ok, err)
}
if err := base.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyPhoneNumber,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleAllowAll}},
}); err != nil {
t.Fatalf("direct set: %v", err)
}
cached.InvalidateOwners(1001)
got, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber)
if err != nil || !ok {
t.Fatalf("after external invalidation ok=%v err=%v", ok, err)
}
if got.Rules[0].Kind != domain.PrivacyRuleAllowAll {
t.Fatalf("after invalidation = %+v, want allow all", got.Rules)
}
if err := base.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyPhoneNumber,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleDisallowAll}},
}); err != nil {
t.Fatalf("direct set 2: %v", err)
}
cached.FlushReadModelCache()
got, ok, err = cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber)
if err != nil || !ok {
t.Fatalf("after flush ok=%v err=%v", ok, err)
}
if got.Rules[0].Kind != domain.PrivacyRuleDisallowAll {
t.Fatalf("after flush = %+v, want disallow all", got.Rules)
}
if counting.listCalls != 3 {
t.Fatalf("ListPrivacyRules calls = %d, want 3 after prime+invalidate+flush", counting.listCalls)
}
}
func TestCachedPrivacyStoreDoesNotRefillStaleSnapshotAfterInvalidation(t *testing.T) {
ctx := context.Background()
base := memory.NewPrivacyStore()
if err := base.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyPhoneNumber,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleDisallowAll}},
}); err != nil {
t.Fatalf("seed privacy: %v", err)
}
first, err := base.ListPrivacyRules(ctx, []int64{1001}, allPrivacyRuleKeys)
if err != nil {
t.Fatalf("snapshot first privacy rules: %v", err)
}
blocking := &blockingFirstPrivacyStore{
PrivacyStore: base,
started: make(chan struct{}),
release: make(chan struct{}),
first: first,
}
cached := NewCachedPrivacyStore(blocking, 0)
type readResult struct {
rules domain.PrivacyRules
ok bool
err error
}
resultCh := make(chan readResult, 1)
go func() {
rules, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber)
resultCh <- readResult{rules: rules, ok: ok, err: err}
}()
waitForPrivacyCacheTestSignal(t, blocking.started)
if err := base.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyPhoneNumber,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleAllowAll}},
}); err != nil {
t.Fatalf("update privacy while first load is blocked: %v", err)
}
cached.InvalidateOwners(1001)
close(blocking.release)
var result readResult
select {
case result = <-resultCh:
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for privacy read")
}
if result.err != nil || !result.ok {
t.Fatalf("privacy read ok=%v err=%v", result.ok, result.err)
}
if result.rules.Rules[0].Kind != domain.PrivacyRuleAllowAll {
t.Fatalf("privacy after concurrent invalidation = %+v, want allow all", result.rules.Rules)
}
cachedHit, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber)
if err != nil || !ok {
t.Fatalf("cached hit after stale load retry ok=%v err=%v", ok, err)
}
if cachedHit.Rules[0].Kind != domain.PrivacyRuleAllowAll {
t.Fatalf("cached privacy after stale load retry = %+v, want allow all", cachedHit.Rules)
}
}
func TestCachedPrivacyStoreDoesNotRefillStaleBatchAfterInvalidation(t *testing.T) {
ctx := context.Background()
base := memory.NewPrivacyStore()
if err := base.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyProfilePhoto,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleDisallowAll}},
}); err != nil {
t.Fatalf("seed privacy: %v", err)
}
first, err := base.ListPrivacyRules(ctx, []int64{1001}, allPrivacyRuleKeys)
if err != nil {
t.Fatalf("snapshot first privacy rules: %v", err)
}
blocking := &blockingFirstPrivacyStore{
PrivacyStore: base,
started: make(chan struct{}),
release: make(chan struct{}),
first: first,
}
cached := NewCachedPrivacyStore(blocking, 0)
type readResult struct {
rules []domain.PrivacyRules
err error
}
resultCh := make(chan readResult, 1)
go func() {
rules, err := cached.ListPrivacyRules(ctx, []int64{1001}, []domain.PrivacyKey{domain.PrivacyKeyProfilePhoto})
resultCh <- readResult{rules: rules, err: err}
}()
waitForPrivacyCacheTestSignal(t, blocking.started)
if err := base.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyProfilePhoto,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleAllowAll}},
}); err != nil {
t.Fatalf("update privacy while first batch load is blocked: %v", err)
}
cached.InvalidateOwners(1001)
close(blocking.release)
var result readResult
select {
case result = <-resultCh:
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for privacy batch read")
}
if result.err != nil {
t.Fatalf("privacy batch read: %v", result.err)
}
if len(result.rules) != 1 || result.rules[0].Rules[0].Kind != domain.PrivacyRuleAllowAll {
t.Fatalf("privacy batch after concurrent invalidation = %+v, want allow all", result.rules)
}
}
func TestCachedPrivacyStoreListUsesBatchOwnerSnapshots(t *testing.T) {
ctx := context.Background()
base := memory.NewPrivacyStore()
if err := base.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyPhoneNumber,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleDisallowAll}},
}); err != nil {
t.Fatalf("seed 1001: %v", err)
}
if err := base.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1002,
Key: domain.PrivacyKeyProfilePhoto,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleAllowContacts}},
}); err != nil {
t.Fatalf("seed 1002: %v", err)
}
counting := &countingPrivacyStore{PrivacyStore: base}
cached := NewCachedPrivacyStore(counting, 0)
keys := []domain.PrivacyKey{domain.PrivacyKeyPhoneNumber, domain.PrivacyKeyProfilePhoto}
first, err := cached.ListPrivacyRules(ctx, []int64{1001, 1002}, keys)
if err != nil {
t.Fatalf("list first: %v", err)
}
second, err := cached.ListPrivacyRules(ctx, []int64{1001, 1002}, keys)
if err != nil {
t.Fatalf("list second: %v", err)
}
if len(first) != 2 || len(second) != 2 {
t.Fatalf("list sizes = %d/%d, want 2/2", len(first), len(second))
}
if counting.listCalls != 1 {
t.Fatalf("ListPrivacyRules calls = %d, want 1 batch load", counting.listCalls)
}
}