package store import ( "context" "testing" "time" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" ) func rdb(t *testing.T) *redis.Client { t.Helper() mr := miniredis.RunT(t) return redis.NewClient(&redis.Options{Addr: mr.Addr()}) } func TestAllowSessionWindow(t *testing.T) { r := rdb(t) ctx := context.Background() now := time.Now() // 30 次放行,第 31 次拒绝 for i := 0; i < 30; i++ { ok, err := AllowSession(ctx, r, "dev1", now) if err != nil || !ok { t.Fatalf("session %d should pass: ok=%v err=%v", i+1, ok, err) } } if ok, _ := AllowSession(ctx, r, "dev1", now); ok { t.Fatal("31st session should be rejected") } // 其他设备不受影响 if ok, _ := AllowSession(ctx, r, "dev2", now); !ok { t.Fatal("other device should pass") } } // TestAudioWindowExhausted 复用生产路径(AudioWindowExhausted 只查 + RecordAudioSeconds 记账)。 func TestAudioWindowExhausted(t *testing.T) { r := rdb(t) ctx := context.Background() now := time.Now() // 初始未满 if full, _ := AudioWindowExhausted(ctx, r, "dev1", now); full { t.Fatal("fresh window should not be exhausted") } // 记满后窗口耗尽(AudioWindowExhausted 只查不记,val=0,sum>limit 才算满) if err := RecordAudioSeconds(ctx, r, "dev1", 1801, now); err != nil { t.Fatal(err) } if full, _ := AudioWindowExhausted(ctx, r, "dev1", now.Add(time.Second)); !full { t.Fatal("window should be exhausted after exceeding 1800s") } // 其他设备不受影响 if full, _ := AudioWindowExhausted(ctx, r, "dev2", now); full { t.Fatal("other device window should not be exhausted") } } func TestDeviceSlot(t *testing.T) { r := rdb(t) ctx := context.Background() ok, err := AcquireDeviceSlot(ctx, r, "dev1", "s1") if err != nil || !ok { t.Fatalf("first acquire should pass: %v", err) } if ok, _ := AcquireDeviceSlot(ctx, r, "dev1", "s2"); ok { t.Fatal("second concurrent acquire should fail") } // 非持有者释放无效 if err := ReleaseDeviceSlot(ctx, r, "dev1", "s2"); err != nil { t.Fatal(err) } if ok, _ := AcquireDeviceSlot(ctx, r, "dev1", "s3"); ok { t.Fatal("slot should still be held by s1") } // 持有者释放后可再获取 if err := ReleaseDeviceSlot(ctx, r, "dev1", "s1"); err != nil { t.Fatal(err) } if ok, _ := AcquireDeviceSlot(ctx, r, "dev1", "s3"); !ok { t.Fatal("acquire after release should pass") } } // TestRefreshDeviceSlot 仅持有者能续期;非持有者续期无效(17D)。 func TestRefreshDeviceSlot(t *testing.T) { r := rdb(t) ctx := context.Background() if ok, err := AcquireDeviceSlot(ctx, r, "dev1", "s1"); err != nil || !ok { t.Fatalf("acquire should pass: %v", err) } // 持有者续期成功 if ok, err := RefreshDeviceSlot(ctx, r, "dev1", "s1"); err != nil || !ok { t.Fatalf("holder refresh should succeed: ok=%v err=%v", ok, err) } // 非持有者续期失败 if ok, _ := RefreshDeviceSlot(ctx, r, "dev1", "s2"); ok { t.Fatal("non-holder refresh should fail") } // 续期未释放槽位:他人仍抢不到 if ok, _ := AcquireDeviceSlot(ctx, r, "dev1", "s2"); ok { t.Fatal("slot should still be held after refresh") } }