owpengram-server/internal/app/userprojection/projection_sparse_test.go
2026-09-01 12:06:31 +03:00

115 lines
3.9 KiB
Go

package userprojection
import (
"context"
"reflect"
"testing"
privacyapp "telesrv/internal/app/privacy"
"telesrv/internal/domain"
"telesrv/internal/store"
"telesrv/internal/store/memory"
)
type recordingSparseContactStore struct {
store.ContactStore
sparseCalls int
denseCalls int
requested map[int64][]int64
}
func (s *recordingSparseContactStore) ContactProjectionForViewers(ctx context.Context, viewers, owners []int64) (domain.ContactProjectionBatch, error) {
s.denseCalls++
return s.ContactStore.ContactProjectionForViewers(ctx, viewers, owners)
}
func (s *recordingSparseContactStore) ContactProjectionForViewerUserIDs(ctx context.Context, requested map[int64][]int64) (domain.ContactProjectionBatch, error) {
s.sparseCalls++
s.requested = make(map[int64][]int64, len(requested))
for viewerID, ids := range requested {
s.requested[viewerID] = append([]int64(nil), ids...)
}
return s.ContactStore.(store.SparseContactProjectionStore).ContactProjectionForViewerUserIDs(ctx, requested)
}
func TestForViewerUserIDsUsesActualPairsAndMatchesScalarProjection(t *testing.T) {
ctx := context.Background()
const (
viewerA = int64(1101)
viewerB = int64(1102)
ownerA = int64(2101)
ownerB = int64(2102)
)
contacts := memory.NewContactStore()
// Seed both requested and cross-viewer rows. A dense matrix would expose the
// cross aliases/photos; the sparse request must never ask for those pairs.
for _, input := range []struct {
viewer int64
owner int64
name string
photo int64
}{
{viewerA, ownerA, "A for viewer A", 9101},
{viewerA, ownerB, "B cross leak", 9191},
{viewerB, ownerB, "B for viewer B", 9102},
{viewerB, ownerA, "A cross leak", 9192},
// Reverse rows are the privacy ViewerIsContact facts.
{ownerA, viewerA, "viewer A", 0},
{ownerB, viewerB, "viewer B", 0},
} {
if _, err := contacts.Upsert(ctx, input.viewer, domain.ContactInput{ContactUserID: input.owner, FirstName: input.name}); err != nil {
t.Fatalf("upsert %d->%d: %v", input.viewer, input.owner, err)
}
if input.photo != 0 {
if _, _, err := contacts.SetPersonalPhoto(ctx, input.viewer, input.owner, input.photo, 100); err != nil {
t.Fatalf("personal photo %d->%d: %v", input.viewer, input.owner, err)
}
}
}
recording := &recordingSparseContactStore{ContactStore: contacts}
privacy := privacyapp.NewService(memory.NewPrivacyStore(), recording)
projector := New(WithContactStore(recording), WithPrivacyEvaluator(privacy))
base := []domain.User{
{ID: ownerA, AccessHash: 31, Phone: "15552101", FirstName: "Owner A"},
{ID: ownerB, AccessHash: 32, Phone: "15552102", FirstName: "Owner B"},
}
wantA, err := projector.ForViewer(ctx, viewerA, base[:1])
if err != nil {
t.Fatalf("scalar viewer A: %v", err)
}
wantB, err := projector.ForViewer(ctx, viewerB, base[1:])
if err != nil {
t.Fatalf("scalar viewer B: %v", err)
}
recording.sparseCalls = 0
recording.denseCalls = 0
recording.requested = nil
got, err := projector.ForViewerUserIDs(ctx, map[int64][]int64{
viewerA: {ownerA},
viewerB: {ownerB},
}, base)
if err != nil {
t.Fatalf("ForViewerUserIDs: %v", err)
}
if !reflect.DeepEqual(got[viewerA], wantA) || !reflect.DeepEqual(got[viewerB], wantB) {
t.Fatalf("sparse projection = %+v, want scalar A=%+v B=%+v", got, wantA, wantB)
}
if recording.sparseCalls != 1 || recording.denseCalls != 0 {
t.Fatalf("contact projection calls = sparse %d dense %d, want 1/0", recording.sparseCalls, recording.denseCalls)
}
wantPairs := map[int64][]int64{
viewerA: {ownerA},
viewerB: {ownerB},
ownerA: {viewerA},
ownerB: {viewerB},
}
for viewerID, ids := range wantPairs {
if !reflect.DeepEqual(recording.requested[viewerID], ids) {
t.Fatalf("requested[%d] = %v, want %v (all=%+v)", viewerID, recording.requested[viewerID], ids, recording.requested)
}
}
if len(recording.requested) != len(wantPairs) {
t.Fatalf("requested pairs = %+v, contains unexpected cross-viewer edges", recording.requested)
}
}