From 2435770e2f780b90dfdbe114d91e6deb27767c59 Mon Sep 17 00:00:00 2001 From: Taras Kushnir Date: Tue, 6 Jan 2026 18:48:08 +0200 Subject: [PATCH] Add tests for in-memory time series --- pkg/db/timeseries_test.go | 243 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 pkg/db/timeseries_test.go diff --git a/pkg/db/timeseries_test.go b/pkg/db/timeseries_test.go new file mode 100644 index 00000000..fa9e6a39 --- /dev/null +++ b/pkg/db/timeseries_test.go @@ -0,0 +1,243 @@ +package db + +import ( + "context" + "testing" + "time" + + "github.com/PrivateCaptcha/PrivateCaptcha/pkg/common" +) + +func TestMemoryTimeSeriesPing(t *testing.T) { + ts := NewMemoryTimeSeries() + if err := ts.Ping(context.Background()); err != nil { + t.Error(err) + } +} + +func TestMemoryTimeSeriesRetrievePropertyStatsSince(t *testing.T) { + ts := NewMemoryTimeSeries() + ctx := context.Background() + // Use a fixed time aligned to 5 minutes to ensure deterministic bucketing + now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) + + records := []*common.AccessRecord{ + { + UserID: 1, + OrgID: 10, + PropertyID: 100, + Timestamp: now, + }, + { + UserID: 1, + OrgID: 10, + PropertyID: 100, + Timestamp: now.Add(1 * time.Minute), + }, + { + UserID: 1, + OrgID: 10, + PropertyID: 100, + Timestamp: now.Add(6 * time.Minute), // Different 5m bucket + }, + { + UserID: 2, + OrgID: 10, + PropertyID: 100, + Timestamp: now, + }, + } + + err := ts.WriteAccessLogBatch(ctx, records) + if err != nil { + t.Fatal(err) + } + + req := &common.BackfillRequest{ + UserID: 1, + OrgID: 10, + PropertyID: 100, + } + // Expecting 2 buckets for User 1. + // Bucket 1: now (truncated to 5m) -> count 2 + // Bucket 2: now+6m (truncated to 5m) -> count 1 + stats, err := ts.RetrievePropertyStatsSince(ctx, req, now.Add(-1*time.Hour)) + if err != nil { + t.Error(err) + } + + if actual := len(stats); actual != 2 { + t.Errorf("RetrievePropertyStatsSince() got %d stats, want 2", actual) + } + + totalCount := uint32(0) + for _, s := range stats { + totalCount += s.Count + } + if totalCount != 3 { + t.Errorf("RetrievePropertyStatsSince() total count = %d, want 3", totalCount) + } +} + +func TestMemoryTimeSeriesRetrieveAccountStats(t *testing.T) { + // Aggregates by month + ts := NewMemoryTimeSeries() + ctx := context.Background() + fixedTime := time.Date(2023, 10, 15, 12, 0, 0, 0, time.UTC) + records := []*common.AccessRecord{ + {UserID: 1, Timestamp: fixedTime}, + {UserID: 1, Timestamp: fixedTime.Add(24 * time.Hour)}, + {UserID: 2, Timestamp: fixedTime}, + } + ts.WriteAccessLogBatch(ctx, records) + + accountStats, err := ts.RetrieveAccountStats(ctx, 1, fixedTime.Add(-24*time.Hour)) + if err != nil { + t.Error(err) + } + + if len(accountStats) != 1 { + t.Errorf("RetrieveAccountStats() got %d stats, want 1", len(accountStats)) + } else { + if accountStats[0].Count != 2 { + t.Errorf("RetrieveAccountStats() count = %d, want 2", accountStats[0].Count) + } + expectedTs := time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC) + if !accountStats[0].Timestamp.Equal(expectedTs) { + t.Errorf("RetrieveAccountStats() timestamp = %v, want %v", accountStats[0].Timestamp, expectedTs) + } + } +} + +func TestMemoryTimeSeriesVerifyLogsAndStatsByPeriod(t *testing.T) { + ts := NewMemoryTimeSeries() + ctx := context.Background() + + now := time.Now().UTC() + + accessRecords := []*common.AccessRecord{ + {OrgID: 1, PropertyID: 1, Timestamp: now.Add(-30 * time.Minute)}, // Today + {OrgID: 1, PropertyID: 1, Timestamp: now.Add(-2 * time.Hour)}, // Today + } + ts.WriteAccessLogBatch(ctx, accessRecords) + + verifyRecords := []*common.VerifyRecord{ + {OrgID: 1, PropertyID: 1, Timestamp: now.Add(-30 * time.Minute), Status: 1}, + } + ts.WriteVerifyLogBatch(ctx, verifyRecords) + + stats, err := ts.RetrievePropertyStatsByPeriod(ctx, 1, 1, common.TimePeriodToday) + if err != nil { + t.Error(err) + } + + totalReq := 0 + totalVer := 0 + for _, s := range stats { + totalReq += s.RequestsCount + totalVer += s.VerifiesCount + } + + if totalReq != 2 { + t.Errorf("RetrievePropertyStatsByPeriod(Today) requests = %d, want 2", totalReq) + } + if totalVer != 1 { + t.Errorf("RetrievePropertyStatsByPeriod(Today) verifies = %d, want 1", totalVer) + } +} + +func TestMemoryTimeSeriesRecentTopProperties(t *testing.T) { + ts := NewMemoryTimeSeries() + ctx := context.Background() + now := time.Now().UTC() + + records := []*common.VerifyRecord{ + {PropertyID: 1, Timestamp: now}, + {PropertyID: 1, Timestamp: now}, + {PropertyID: 2, Timestamp: now}, + {PropertyID: 3, Timestamp: now.Add(-48 * time.Hour)}, // Too old + } + + ts.WriteVerifyLogBatch(ctx, records) + + top, err := ts.RetrieveRecentTopProperties(ctx, 10) + if err != nil { + t.Error(err) + } + + if len(top) != 2 { + t.Errorf("RetrieveRecentTopProperties() got %d properties, want 2", len(top)) + } + + if top[1] != 2 { + t.Errorf("Property 1 count = %d, want 2", top[1]) + } + if top[2] != 1 { + t.Errorf("Property 2 count = %d, want 1", top[2]) + } + if _, ok := top[3]; ok { + t.Errorf("Property 3 should not be in top list (too old)") + } +} + +func TestMemoryTimeSeriesDeletePropertiesData(t *testing.T) { + ts := NewMemoryTimeSeries() + ctx := context.Background() + + // Populate + ts.WriteAccessLogBatch(ctx, []*common.AccessRecord{ + {UserID: 1, OrgID: 10, PropertyID: 100}, + {UserID: 2, OrgID: 20, PropertyID: 200}, + {UserID: 3, OrgID: 30, PropertyID: 300}, + }) + ts.WriteVerifyLogBatch(ctx, []*common.VerifyRecord{ + {UserID: 1, OrgID: 10, PropertyID: 100}, + {UserID: 2, OrgID: 20, PropertyID: 200}, + {UserID: 3, OrgID: 30, PropertyID: 300}, + }) + + if err := ts.DeletePropertiesData(ctx, []int32{100}); err != nil { + t.Error(err) + } + + stats, _ := ts.RetrievePropertyStatsSince(ctx, &common.BackfillRequest{UserID: 1, OrgID: 10, PropertyID: 100}, time.Time{}) + if len(stats) != 0 { + t.Errorf("After DeletePropertiesData, stats count = %d, want 0", len(stats)) + } +} + +func TestMemoryTimeSeriesDeleteAccountData(t *testing.T) { + ts := NewMemoryTimeSeries() + ctx := context.Background() + + // Populate + ts.WriteAccessLogBatch(ctx, []*common.AccessRecord{ + {UserID: 1, OrgID: 10, PropertyID: 100}, + {UserID: 2, OrgID: 20, PropertyID: 200}, + {UserID: 3, OrgID: 30, PropertyID: 300}, + }) + ts.WriteVerifyLogBatch(ctx, []*common.VerifyRecord{ + {UserID: 1, OrgID: 10, PropertyID: 100}, + {UserID: 2, OrgID: 20, PropertyID: 200}, + {UserID: 3, OrgID: 30, PropertyID: 300}, + }) + + if err := ts.DeleteOrganizationsData(ctx, []int32{20}); err != nil { + t.Error(err) + } + // Check user 2 (Org 20) + stats2, _ := ts.RetrieveAccountStats(ctx, 2, time.Time{}) + if len(stats2) != 0 { + t.Errorf("After DeleteOrganizationsData, stats count = %d, want 0", len(stats2)) + } + + // Delete User 3 + if err := ts.DeleteUsersData(ctx, []int32{3}); err != nil { + t.Errorf("DeleteUsersData error = %v", err) + } + // Check user 3 + stats3, _ := ts.RetrieveAccountStats(ctx, 3, time.Time{}) + if len(stats3) != 0 { + t.Errorf("After DeleteUsersData, stats count = %d, want 0", len(stats3)) + } +}