A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
at main 10 kB view raw
1// Import the mocked redis first to get a reference to the mock client 2import { createClient } from "redis"; 3import { afterEach, describe, expect, it, vi } from "vitest"; 4import { logger } from "../logger.js"; 5// Import the modules to be tested 6import { 7 connectRedis, 8 disconnectRedis, 9 getPostLabelCountInWindow, 10 getStarterPackCountInWindow, 11 trackPostLabelForAccount, 12 trackStarterPackForAccount, 13 tryClaimAccountLabel, 14 tryClaimPostLabel, 15} from "../redis.js"; 16 17// Mock the 'redis' module in a way that avoids hoisting issues. 18// The mock implementation is self-contained. 19vi.mock("redis", () => { 20 const mockClient = { 21 on: vi.fn(), 22 connect: vi.fn(), 23 quit: vi.fn(), 24 exists: vi.fn(), 25 set: vi.fn(), 26 zAdd: vi.fn(), 27 zRemRangeByScore: vi.fn(), 28 zCount: vi.fn(), 29 expire: vi.fn(), 30 }; 31 return { 32 createClient: vi.fn(() => mockClient), 33 }; 34}); 35 36const mockRedisClient = createClient(); 37 38// Suppress logger output during tests 39vi.mock("../logger.js", () => ({ 40 logger: { 41 info: vi.fn(), 42 warn: vi.fn(), 43 error: vi.fn(), 44 debug: vi.fn(), 45 }, 46})); 47 48describe("Redis Cache Logic", () => { 49 afterEach(() => { 50 vi.clearAllMocks(); 51 }); 52 53 describe("Connection", () => { 54 it("should call redisClient.connect on connectRedis", async () => { 55 await connectRedis(); 56 expect(mockRedisClient.connect).toHaveBeenCalled(); 57 }); 58 59 it("should call redisClient.quit on disconnectRedis", async () => { 60 await disconnectRedis(); 61 expect(mockRedisClient.quit).toHaveBeenCalled(); 62 }); 63 }); 64 65 describe("tryClaimPostLabel", () => { 66 it("should return true and set key if key does not exist", async () => { 67 vi.mocked(mockRedisClient.set).mockResolvedValue("OK"); 68 const result = await tryClaimPostLabel("at://uri", "test-label"); 69 expect(result).toBe(true); 70 expect(mockRedisClient.set).toHaveBeenCalledWith( 71 "post-label:at://uri:test-label", 72 "1", 73 { NX: true, EX: 60 * 60 * 24 * 7 }, 74 ); 75 }); 76 77 it("should return false if key already exists", async () => { 78 vi.mocked(mockRedisClient.set).mockResolvedValue(null); 79 const result = await tryClaimPostLabel("at://uri", "test-label"); 80 expect(result).toBe(false); 81 }); 82 83 it("should return true and log warning on Redis error", async () => { 84 const redisError = new Error("Redis down"); 85 vi.mocked(mockRedisClient.set).mockRejectedValue(redisError); 86 const result = await tryClaimPostLabel("at://uri", "test-label"); 87 expect(result).toBe(true); 88 expect(logger.warn).toHaveBeenCalledWith( 89 { err: redisError, atURI: "at://uri", label: "test-label" }, 90 "Error claiming post label in Redis, allowing through", 91 ); 92 }); 93 }); 94 95 describe("tryClaimAccountLabel", () => { 96 it("should return true and set key if key does not exist", async () => { 97 vi.mocked(mockRedisClient.set).mockResolvedValue("OK"); 98 const result = await tryClaimAccountLabel("did:plc:123", "test-label"); 99 expect(result).toBe(true); 100 expect(mockRedisClient.set).toHaveBeenCalledWith( 101 "account-label:did:plc:123:test-label", 102 "1", 103 { NX: true, EX: 60 * 60 * 24 * 7 }, 104 ); 105 }); 106 107 it("should return false if key already exists", async () => { 108 vi.mocked(mockRedisClient.set).mockResolvedValue(null); 109 const result = await tryClaimAccountLabel("did:plc:123", "test-label"); 110 expect(result).toBe(false); 111 }); 112 }); 113 114 describe("trackPostLabelForAccount", () => { 115 it("should track post label with correct timestamp and TTL", async () => { 116 vi.mocked(mockRedisClient.zRemRangeByScore).mockResolvedValue(0); 117 vi.mocked(mockRedisClient.zAdd).mockResolvedValue(1); 118 vi.mocked(mockRedisClient.expire).mockResolvedValue(true); 119 120 const timestamp = 1640000000000000; // microseconds 121 const window = 5; 122 const windowUnit = "days" as const; 123 124 await trackPostLabelForAccount( 125 "did:plc:123", 126 "test-label", 127 timestamp, 128 window, 129 windowUnit, 130 ); 131 132 const expectedKey = "account-post-labels:did:plc:123:test-label:5days"; 133 const windowStartTime = timestamp - window * 24 * 60 * 60 * 1000000; 134 135 expect(mockRedisClient.zRemRangeByScore).toHaveBeenCalledWith( 136 expectedKey, 137 "-inf", 138 windowStartTime, 139 ); 140 expect(mockRedisClient.zAdd).toHaveBeenCalledWith(expectedKey, { 141 score: timestamp, 142 value: timestamp.toString(), 143 }); 144 expect(mockRedisClient.expire).toHaveBeenCalledWith( 145 expectedKey, 146 window * 24 * 60 * 60 + 60 * 60, 147 ); 148 }); 149 150 it("should throw error on Redis failure", async () => { 151 const redisError = new Error("Redis down"); 152 vi.mocked(mockRedisClient.zRemRangeByScore).mockRejectedValue(redisError); 153 154 await expect( 155 trackPostLabelForAccount( 156 "did:plc:123", 157 "test-label", 158 1640000000000000, 159 5, 160 "days", 161 ), 162 ).rejects.toThrow("Redis down"); 163 164 expect(logger.error).toHaveBeenCalled(); 165 }); 166 }); 167 168 describe("trackStarterPackForAccount", () => { 169 it("should track starter pack with correct timestamp and TTL", async () => { 170 vi.mocked(mockRedisClient.zRemRangeByScore).mockResolvedValue(0); 171 vi.mocked(mockRedisClient.zAdd).mockResolvedValue(1); 172 vi.mocked(mockRedisClient.expire).mockResolvedValue(true); 173 174 const timestamp = 1640000000000000; 175 const window = 24; 176 const windowUnit = "hours" as const; 177 178 await trackStarterPackForAccount( 179 "did:plc:123", 180 "at://did:plc:123/app.bsky.graph.starterpack/abc", 181 timestamp, 182 window, 183 windowUnit, 184 ); 185 186 const expectedKey = "starterpack:threshold:did:plc:123:24hours"; 187 const windowStartTime = timestamp - window * 60 * 60 * 1000000; 188 189 expect(mockRedisClient.zRemRangeByScore).toHaveBeenCalledWith( 190 expectedKey, 191 "-inf", 192 windowStartTime, 193 ); 194 expect(mockRedisClient.zAdd).toHaveBeenCalledWith(expectedKey, { 195 score: timestamp, 196 value: "at://did:plc:123/app.bsky.graph.starterpack/abc", 197 }); 198 expect(mockRedisClient.expire).toHaveBeenCalledWith( 199 expectedKey, 200 window * 60 * 60 + 60 * 60, 201 ); 202 }); 203 204 it("should throw error on Redis failure", async () => { 205 const redisError = new Error("Redis down"); 206 vi.mocked(mockRedisClient.zRemRangeByScore).mockRejectedValue(redisError); 207 208 await expect( 209 trackStarterPackForAccount( 210 "did:plc:123", 211 "at://did:plc:123/app.bsky.graph.starterpack/abc", 212 1640000000000000, 213 24, 214 "hours", 215 ), 216 ).rejects.toThrow("Redis down"); 217 218 expect(logger.error).toHaveBeenCalled(); 219 }); 220 }); 221 222 describe("getStarterPackCountInWindow", () => { 223 it("should count starter packs in window", async () => { 224 vi.mocked(mockRedisClient.zCount).mockResolvedValue(3); 225 226 const currentTime = 1640000000000000; 227 const window = 24; 228 const windowUnit = "hours" as const; 229 const count = await getStarterPackCountInWindow( 230 "did:plc:123", 231 window, 232 windowUnit, 233 currentTime, 234 ); 235 236 expect(count).toBe(3); 237 const windowStartTime = currentTime - window * 60 * 60 * 1000000; 238 expect(mockRedisClient.zCount).toHaveBeenCalledWith( 239 "starterpack:threshold:did:plc:123:24hours", 240 windowStartTime, 241 "+inf", 242 ); 243 }); 244 245 it("should throw error on Redis failure", async () => { 246 const redisError = new Error("Redis down"); 247 vi.mocked(mockRedisClient.zCount).mockRejectedValue(redisError); 248 249 await expect( 250 getStarterPackCountInWindow("did:plc:123", 24, "hours", 1640000000000000), 251 ).rejects.toThrow("Redis down"); 252 253 expect(logger.error).toHaveBeenCalled(); 254 }); 255 }); 256 257 describe("getPostLabelCountInWindow", () => { 258 it("should count posts for single label", async () => { 259 vi.mocked(mockRedisClient.zCount).mockResolvedValue(3); 260 261 const currentTime = 1640000000000000; 262 const window = 5; 263 const windowUnit = "days" as const; 264 const count = await getPostLabelCountInWindow( 265 "did:plc:123", 266 ["test-label"], 267 window, 268 windowUnit, 269 currentTime, 270 ); 271 272 expect(count).toBe(3); 273 const windowStartTime = currentTime - window * 24 * 60 * 60 * 1000000; 274 expect(mockRedisClient.zCount).toHaveBeenCalledWith( 275 "account-post-labels:did:plc:123:test-label:5days", 276 windowStartTime, 277 "+inf", 278 ); 279 }); 280 281 it("should sum counts for multiple labels (OR logic)", async () => { 282 vi.mocked(mockRedisClient.zCount) 283 .mockResolvedValueOnce(3) 284 .mockResolvedValueOnce(2) 285 .mockResolvedValueOnce(1); 286 287 const currentTime = 1640000000000000; 288 const window = 5; 289 const windowUnit = "days" as const; 290 const count = await getPostLabelCountInWindow( 291 "did:plc:123", 292 ["label-1", "label-2", "label-3"], 293 window, 294 windowUnit, 295 currentTime, 296 ); 297 298 expect(count).toBe(6); 299 expect(mockRedisClient.zCount).toHaveBeenCalledTimes(3); 300 }); 301 302 it("should return 0 when no posts in window", async () => { 303 vi.mocked(mockRedisClient.zCount).mockResolvedValue(0); 304 305 const count = await getPostLabelCountInWindow( 306 "did:plc:123", 307 ["test-label"], 308 5, 309 "days", 310 1640000000000000, 311 ); 312 313 expect(count).toBe(0); 314 }); 315 316 it("should throw error on Redis failure", async () => { 317 const redisError = new Error("Redis down"); 318 vi.mocked(mockRedisClient.zCount).mockRejectedValue(redisError); 319 320 await expect( 321 getPostLabelCountInWindow( 322 "did:plc:123", 323 ["test-label"], 324 5, 325 "days", 326 1640000000000000, 327 ), 328 ).rejects.toThrow("Redis down"); 329 330 expect(logger.error).toHaveBeenCalled(); 331 }); 332 }); 333});