mirror of
https://github.com/nprasad2077/NBA_Go.git
synced 2026-09-22 14:05:13 +00:00
296 lines
7.4 KiB
Go
296 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParsePageMixDistributesRangeWeights(t *testing.T) {
|
|
buckets, err := parsePageMix("1-3:60,4-10:30,11-20:10")
|
|
if err != nil {
|
|
t.Fatalf("parsePageMix returned an error: %v", err)
|
|
}
|
|
|
|
if len(buckets) != 20 {
|
|
t.Fatalf("expected 20 page buckets, got %d", len(buckets))
|
|
}
|
|
|
|
for page := 1; page <= 20; page++ {
|
|
bucket := buckets[page-1]
|
|
if bucket.page != page {
|
|
t.Errorf("bucket %d has page %d", page-1, bucket.page)
|
|
}
|
|
|
|
expectedWeight := 1.0
|
|
if page <= 3 {
|
|
expectedWeight = 20
|
|
} else if page <= 10 {
|
|
expectedWeight = 30.0 / 7
|
|
}
|
|
if bucket.weight != expectedWeight {
|
|
t.Errorf("page %d has weight %v, want %v", page, bucket.weight, expectedWeight)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParsePageMixRejectsInvalidSpecifications(t *testing.T) {
|
|
invalidSpecs := []string{
|
|
"1-3",
|
|
"0:1",
|
|
"3-1:1",
|
|
"1-3:0",
|
|
"letters:1",
|
|
"1:1,1:2",
|
|
}
|
|
|
|
for _, spec := range invalidSpecs {
|
|
t.Run(spec, func(t *testing.T) {
|
|
if _, err := parsePageMix(spec); err == nil {
|
|
t.Fatalf("parsePageMix(%q) returned nil error", spec)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSelectPagesUsesConfiguredWeights(t *testing.T) {
|
|
buckets, err := parsePageMix("1-3:60,4-10:30,11-20:10")
|
|
if err != nil {
|
|
t.Fatalf("parsePageMix returned an error: %v", err)
|
|
}
|
|
|
|
pages := selectPages(buckets, 10000, 42)
|
|
if len(pages) != 10000 {
|
|
t.Fatalf("expected 10000 selected pages, got %d", len(pages))
|
|
}
|
|
|
|
counts := make(map[int]int)
|
|
for _, page := range pages {
|
|
counts[page]++
|
|
}
|
|
|
|
assertCountNear(t, "pages 1-3", counts[1]+counts[2]+counts[3], 6000, 150)
|
|
assertCountNear(t, "pages 4-10", sumPageCounts(counts, 4, 10), 3000, 150)
|
|
assertCountNear(t, "pages 11-20", sumPageCounts(counts, 11, 20), 1000, 100)
|
|
}
|
|
|
|
func TestSelectPagesIsReproducibleWithSeed(t *testing.T) {
|
|
buckets, err := parsePageMix("1:3,2:1")
|
|
if err != nil {
|
|
t.Fatalf("parsePageMix returned an error: %v", err)
|
|
}
|
|
|
|
first := selectPages(buckets, 20, 42)
|
|
second := selectPages(buckets, 20, 42)
|
|
if !reflect.DeepEqual(first, second) {
|
|
t.Fatalf("page selections differ for the same seed: %v != %v", first, second)
|
|
}
|
|
}
|
|
|
|
func TestURLForPageReplacesOnlyPageParameter(t *testing.T) {
|
|
base, err := url.Parse("https://example.test/api/playertotals?page=1&pageSize=50&season=2025")
|
|
if err != nil {
|
|
t.Fatalf("url.Parse returned an error: %v", err)
|
|
}
|
|
|
|
requestURL, err := url.Parse(urlForPage(*base, 7))
|
|
if err != nil {
|
|
t.Fatalf("url.Parse returned an error: %v", err)
|
|
}
|
|
|
|
query := requestURL.Query()
|
|
if query.Get("page") != "7" {
|
|
t.Errorf("page query parameter = %q, want 7", query.Get("page"))
|
|
}
|
|
if query.Get("pageSize") != "50" {
|
|
t.Errorf("pageSize query parameter = %q, want 50", query.Get("pageSize"))
|
|
}
|
|
if query.Get("season") != "2025" {
|
|
t.Errorf("season query parameter = %q, want 2025", query.Get("season"))
|
|
}
|
|
}
|
|
|
|
func TestParseEndpointMix(t *testing.T) {
|
|
buckets, err := parseEndpointMix("playeradvancedstats:40,playertotals:30,games:30")
|
|
if err != nil {
|
|
t.Fatalf("parseEndpointMix error: %v", err)
|
|
}
|
|
if len(buckets) != 3 {
|
|
t.Fatalf("expected 3 buckets, got %d", len(buckets))
|
|
}
|
|
if buckets[0].endpoint != "/api/playeradvancedstats" || buckets[0].weight != 40 {
|
|
t.Errorf("bucket 0 = %+v", buckets[0])
|
|
}
|
|
if buckets[1].endpoint != "/api/playertotals" || buckets[1].weight != 30 {
|
|
t.Errorf("bucket 1 = %+v", buckets[1])
|
|
}
|
|
if buckets[2].endpoint != "/api/games" || buckets[2].weight != 30 {
|
|
t.Errorf("bucket 2 = %+v", buckets[2])
|
|
}
|
|
}
|
|
|
|
func TestGenerateRequestURLVariesParams(t *testing.T) {
|
|
baseURL, _ := url.Parse("https://nba.turbo-data.com/api/playeradvancedstats")
|
|
generatedURLs := make(map[string]bool)
|
|
|
|
for i := 0; i < 50; i++ {
|
|
rng := rand.New(rand.NewSource(int64(i + 100)))
|
|
u := GenerateRequestURL(RequestOptions{
|
|
BaseURL: *baseURL,
|
|
Page: 0,
|
|
VaryParams: true,
|
|
Complexity: "high",
|
|
CacheBust: false,
|
|
RequestID: i,
|
|
WorkerID: 1,
|
|
RNG: rng,
|
|
})
|
|
generatedURLs[u] = true
|
|
parsed, err := url.Parse(u)
|
|
if err != nil {
|
|
t.Fatalf("invalid url generated: %s", u)
|
|
}
|
|
q := parsed.Query()
|
|
if q.Get("sortBy") == "" {
|
|
t.Errorf("expected sortBy to be populated, got URL: %s", u)
|
|
}
|
|
if q.Get("pageSize") == "" {
|
|
t.Errorf("expected pageSize to be populated, got URL: %s", u)
|
|
}
|
|
}
|
|
|
|
if len(generatedURLs) < 40 {
|
|
t.Errorf("expected high URL diversity (>40 distinct URLs for 50 requests), got %d", len(generatedURLs))
|
|
}
|
|
}
|
|
|
|
func TestGenerateRequestURLCacheBust(t *testing.T) {
|
|
baseURL, _ := url.Parse("https://nba.turbo-data.com/api/playeradvancedstats?page=1&pageSize=40")
|
|
rng := rand.New(rand.NewSource(42))
|
|
|
|
u1 := GenerateRequestURL(RequestOptions{
|
|
BaseURL: *baseURL,
|
|
Page: 1,
|
|
CacheBust: true,
|
|
RequestID: 1,
|
|
WorkerID: 1,
|
|
RNG: rng,
|
|
})
|
|
|
|
u2 := GenerateRequestURL(RequestOptions{
|
|
BaseURL: *baseURL,
|
|
Page: 1,
|
|
CacheBust: true,
|
|
RequestID: 2,
|
|
WorkerID: 1,
|
|
RNG: rng,
|
|
})
|
|
|
|
if u1 == u2 {
|
|
t.Errorf("expected distinct URLs with cacheBust=true, got identical: %s", u1)
|
|
}
|
|
|
|
p1, _ := url.Parse(u1)
|
|
if p1.Query().Get("_cb") == "" {
|
|
t.Errorf("expected _cb parameter in URL %s", u1)
|
|
}
|
|
}
|
|
|
|
func TestExtractCacheStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
headers http.Header
|
|
expected CacheStatus
|
|
}{
|
|
{
|
|
name: "Cloudflare HIT",
|
|
headers: http.Header{"Cf-Cache-Status": []string{"HIT"}},
|
|
expected: CacheHit,
|
|
},
|
|
{
|
|
name: "Cloudflare MISS",
|
|
headers: http.Header{"Cf-Cache-Status": []string{"MISS"}},
|
|
expected: CacheMiss,
|
|
},
|
|
{
|
|
name: "Cloudflare DYNAMIC",
|
|
headers: http.Header{"Cf-Cache-Status": []string{"DYNAMIC"}},
|
|
expected: CacheMiss,
|
|
},
|
|
{
|
|
name: "X-Cache HIT",
|
|
headers: http.Header{"X-Cache": []string{"HIT from proxy"}},
|
|
expected: CacheHit,
|
|
},
|
|
{
|
|
name: "Age header > 0",
|
|
headers: http.Header{"Age": []string{"120"}},
|
|
expected: CacheHit,
|
|
},
|
|
{
|
|
name: "Age header 0 with no cache headers",
|
|
headers: http.Header{"Age": []string{"0"}},
|
|
expected: CacheUnknown,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
actual := ExtractCacheStatus(tt.headers)
|
|
if actual != tt.expected {
|
|
t.Errorf("ExtractCacheStatus() = %v, want %v", actual, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCalculatePercentiles(t *testing.T) {
|
|
durations := []time.Duration{
|
|
10 * time.Millisecond,
|
|
20 * time.Millisecond,
|
|
30 * time.Millisecond,
|
|
40 * time.Millisecond,
|
|
50 * time.Millisecond,
|
|
60 * time.Millisecond,
|
|
70 * time.Millisecond,
|
|
80 * time.Millisecond,
|
|
90 * time.Millisecond,
|
|
100 * time.Millisecond,
|
|
}
|
|
|
|
stats := CalculatePercentiles(durations)
|
|
if stats.Count != 10 {
|
|
t.Errorf("Count = %d, want 10", stats.Count)
|
|
}
|
|
if stats.Min != 10*time.Millisecond {
|
|
t.Errorf("Min = %v, want 10ms", stats.Min)
|
|
}
|
|
if stats.Max != 100*time.Millisecond {
|
|
t.Errorf("Max = %v, want 100ms", stats.Max)
|
|
}
|
|
if stats.Average != 55*time.Millisecond {
|
|
t.Errorf("Average = %v, want 55ms", stats.Average)
|
|
}
|
|
if stats.P50 != 55*time.Millisecond {
|
|
t.Errorf("P50 = %v, want 55ms", stats.P50)
|
|
}
|
|
}
|
|
|
|
func assertCountNear(t *testing.T, label string, actual, expected, tolerance int) {
|
|
t.Helper()
|
|
if actual < expected-tolerance || actual > expected+tolerance {
|
|
t.Errorf("%s count = %d, want %d +/- %d", label, actual, expected, tolerance)
|
|
}
|
|
}
|
|
|
|
func sumPageCounts(counts map[int]int, firstPage, lastPage int) int {
|
|
total := 0
|
|
for page := firstPage; page <= lastPage; page++ {
|
|
total += counts[page]
|
|
}
|
|
return total
|
|
}
|